Compare commits
7 Commits
08b74a0294
...
e9d8fb95cf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9d8fb95cf | ||
|
|
aa4629752a | ||
|
|
8197308cef | ||
|
|
c136e930c9 | ||
|
|
c51629b229 | ||
|
|
be3a8a82ab | ||
|
|
dfa357bbd1 |
104
app/data/site.config.json
Normal file
104
app/data/site.config.json
Normal 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
10
app/data/smtp.config.json
Normal 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
|
||||||
|
}
|
||||||
7
app/data/theme.config.json
Normal file
7
app/data/theme.config.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"activePreset": "sunsetCoral",
|
||||||
|
"customColors": {
|
||||||
|
"bg": "#0B0F19",
|
||||||
|
"accent": "#38BDF8"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
// @ts-check
|
|
||||||
import { defineConfig } from 'astro/config';
|
import { defineConfig } from 'astro/config';
|
||||||
|
|
||||||
import tailwind from '@astrojs/tailwind';
|
import tailwind from '@astrojs/tailwind';
|
||||||
|
|
||||||
import react from '@astrojs/react';
|
import react from '@astrojs/react';
|
||||||
|
import node from '@astrojs/node';
|
||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
output: 'server',
|
||||||
|
adapter: node({
|
||||||
|
mode: 'standalone'
|
||||||
|
}),
|
||||||
integrations: [tailwind(), react()]
|
integrations: [tailwind(), react()]
|
||||||
});
|
});
|
||||||
63
data_template/site.config.json
Normal file
63
data_template/site.config.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
10
data_template/smtp.config.json
Normal file
10
data_template/smtp.config.json
Normal 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
|
||||||
|
}
|
||||||
7
data_template/theme.config.json
Normal file
7
data_template/theme.config.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"activePreset": "corporateDark",
|
||||||
|
"customColors": {
|
||||||
|
"bg": "#0B0F19",
|
||||||
|
"accent": "#38BDF8"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,4 +6,11 @@ services:
|
|||||||
container_name: websitedummy_app
|
container_name: websitedummy_app
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
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
1412
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@
|
|||||||
"astro": "astro"
|
"astro": "astro"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@astrojs/node": "^8.3.4",
|
||||||
"@astrojs/react": "^3.6.3",
|
"@astrojs/react": "^3.6.3",
|
||||||
"@astrojs/tailwind": "^5.1.4",
|
"@astrojs/tailwind": "^5.1.4",
|
||||||
"@fontsource/inter": "^5.2.8",
|
"@fontsource/inter": "^5.2.8",
|
||||||
@@ -19,9 +20,15 @@
|
|||||||
"@types/react": "^19.2.17",
|
"@types/react": "^19.2.17",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"astro": "^4.16.0",
|
"astro": "^4.16.0",
|
||||||
|
"nodemailer": "^9.0.5",
|
||||||
"react": "^19.2.7",
|
"react": "^19.2.7",
|
||||||
"react-dom": "^19.2.7",
|
"react-dom": "^19.2.7",
|
||||||
"react-icons": "^5.7.0",
|
"react-icons": "^5.7.0",
|
||||||
"tailwindcss": "^3.4.19"
|
"tailwindcss": "^3.4.19"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/nodemailer": "^8.0.1",
|
||||||
|
"@vitest/coverage-v8": "^4.1.10",
|
||||||
|
"vitest": "^4.1.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
82
src/components/DenseBg.astro
Normal file
82
src/components/DenseBg.astro
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
import { animate } from 'motion';
|
||||||
|
---
|
||||||
|
<canvas id="dense-bg" class="dense-bg"></canvas>
|
||||||
|
<script type="module">
|
||||||
|
// Rocky Grund‑Setup
|
||||||
|
const canvas = document.getElementById('dense-bg');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const TAU = Math.PI * 2;
|
||||||
|
const DENSITY = 200; // Rocky Partikel‑Zahl
|
||||||
|
const particles = [];
|
||||||
|
// Rocky Größe anpassen
|
||||||
|
function resize() {
|
||||||
|
canvas.width = innerWidth;
|
||||||
|
canvas.height = innerHeight;
|
||||||
|
}
|
||||||
|
resize();
|
||||||
|
addEventListener('resize', resize);
|
||||||
|
// Rocky Partikel‑Objekt
|
||||||
|
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 Maus‑Tracking
|
||||||
|
let mouse = { x: -9999, y: -9999 };
|
||||||
|
addEventListener('mousemove', e => {
|
||||||
|
mouse.x = e.clientX;
|
||||||
|
mouse.y = e.clientY;
|
||||||
|
});
|
||||||
|
// Rocky Update‑Loop
|
||||||
|
function update() {
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
particles.forEach(p => {
|
||||||
|
// Rocky Bewegung
|
||||||
|
p.x += p.vx;
|
||||||
|
p.y += p.vy;
|
||||||
|
// Rocky Rand‑Wrap
|
||||||
|
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 Maus‑Anziehung
|
||||||
|
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 Fade‑In/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>
|
||||||
|
---
|
||||||
@@ -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>
|
<span class="text-[9px] font-mono text-slate-400 dark:text-zinc-600 mt-6 tracking-wider">Abmahnsicher</span>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -121,13 +121,14 @@ import { FaBars, FaTimes } from 'react-icons/fa';
|
|||||||
updateToggleIcons();
|
updateToggleIcons();
|
||||||
|
|
||||||
themeToggleBtn?.addEventListener('click', () => {
|
themeToggleBtn?.addEventListener('click', () => {
|
||||||
// Toggle class
|
if (document.documentElement.classList.contains('light')) {
|
||||||
if (document.documentElement.classList.contains('dark')) {
|
document.documentElement.classList.remove('light');
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
localStorage.setItem('theme', 'light');
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.add('dark');
|
document.documentElement.classList.add('dark');
|
||||||
localStorage.setItem('theme', 'dark');
|
localStorage.setItem('theme', 'dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
document.documentElement.classList.add('light');
|
||||||
|
localStorage.setItem('theme', 'light');
|
||||||
}
|
}
|
||||||
updateToggleIcons();
|
updateToggleIcons();
|
||||||
});
|
});
|
||||||
|
|||||||
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>
|
||||||
@@ -8,13 +8,20 @@ import '@fontsource/space-grotesk/400.css';
|
|||||||
import '@fontsource/space-grotesk/500.css';
|
import '@fontsource/space-grotesk/500.css';
|
||||||
import '@fontsource/space-grotesk/600.css';
|
import '@fontsource/space-grotesk/600.css';
|
||||||
import '@fontsource/space-grotesk/700.css';
|
import '@fontsource/space-grotesk/700.css';
|
||||||
|
import '../styles/glassmorphism.css';
|
||||||
|
import { loadWaasConfigs } from '../utils/configLoader';
|
||||||
|
import { THEME_PRESETS } from '../utils/themePresets';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
description?: 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>
|
<!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" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<title>{title}</title>
|
<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>
|
<script is:inline>
|
||||||
(function() {
|
(function() {
|
||||||
const theme = localStorage.getItem('theme');
|
const theme = localStorage.getItem('theme');
|
||||||
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
if (theme === 'light') {
|
||||||
document.documentElement.classList.add('dark');
|
document.documentElement.classList.add('light');
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('dark');
|
document.documentElement.classList.remove('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
document.documentElement.classList.remove('light');
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</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 />
|
<slot />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
136
src/pages/admin/editor.astro
Normal file
136
src/pages/admin/editor.astro
Normal file
File diff suppressed because one or more lines are too long
224
src/pages/admin/index.astro
Normal file
224
src/pages/admin/index.astro
Normal 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
39
src/pages/api/config.ts
Normal 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
30
src/pages/api/contact.ts
Normal 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' } });
|
||||||
|
}
|
||||||
|
};
|
||||||
45
src/pages/api/update-config.ts
Normal file
45
src/pages/api/update-config.ts
Normal 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' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -2,56 +2,47 @@
|
|||||||
import Layout from '../layouts/Layout.astro';
|
import Layout from '../layouts/Layout.astro';
|
||||||
import Navbar from '../components/Navbar.astro';
|
import Navbar from '../components/Navbar.astro';
|
||||||
import Footer from '../components/Footer.astro';
|
import Footer from '../components/Footer.astro';
|
||||||
import HeroAndLiquidGlass from '../components/HeroAndLiquidGlass.astro';
|
import HeroSection from '../components/sections/HeroSection.astro';
|
||||||
import OnboardingWizard from '../components/OnboardingWizard.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">
|
<Layout title={siteConfig.siteName} description={siteConfig.metaDescription}>
|
||||||
<Navbar />
|
<div
|
||||||
|
class="min-h-screen transition-colors duration-500 font-sans relative overflow-hidden"
|
||||||
<main>
|
style={`background-color: ${themeConfig.customColors?.bg || colors.bg}; color: ${colors.text};`}
|
||||||
{/* Clean, modular Hero and Liquid Glass section */}
|
>
|
||||||
<HeroAndLiquidGlass />
|
<Navbar />
|
||||||
|
|
||||||
<!-- Multi-step Onboarding Wizard -->
|
<main>
|
||||||
<OnboardingWizard />
|
{siteConfig.sectionsOrder.map((sectionKey) => {
|
||||||
|
if (sectionKey === 'hero') {
|
||||||
<!-- Contact section (separate, clean layout) -->
|
return <HeroSection config={siteConfig.hero} />;
|
||||||
<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>
|
if (sectionKey === 'bento' || sectionKey === 'services') {
|
||||||
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
return <BentoGrid config={siteConfig.bento} />;
|
||||||
<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>
|
if (sectionKey === 'contact') {
|
||||||
<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>
|
return <ContactSection contactConfig={siteConfig.contact} smtpConfig={smtpConfig} />;
|
||||||
<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>
|
return <FallbackSection sectionName={sectionKey} />;
|
||||||
|
})}
|
||||||
<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>
|
</main>
|
||||||
<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.
|
<Footer />
|
||||||
</p>
|
</div>
|
||||||
|
|
||||||
<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>
|
</Layout>
|
||||||
|
|||||||
48
src/styles/glassmorphism.css
Normal file
48
src/styles/glassmorphism.css
Normal 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
199
src/utils/configLoader.ts
Normal 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
124
src/utils/themePresets.ts
Normal 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
53
tests/waasEngine.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user