Compare commits
76 Commits
11e836fc30
...
staging
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1939563ef2 | ||
|
|
698716c34e | ||
|
|
488e024451 | ||
|
|
9c166817fa | ||
|
|
4b7f3f554b | ||
|
|
db2f607e5a | ||
|
|
950497311c | ||
|
|
28473bbcdc | ||
|
|
fc2017b210 | ||
|
|
831a0d53f0 | ||
|
|
9284717a80 | ||
|
|
4d072864bc | ||
|
|
e932495742 | ||
|
|
6042c5ad23 | ||
|
|
041cf20ca3 | ||
|
|
4355bb8406 | ||
|
|
30b407d2e8 | ||
|
|
591eba43c1 | ||
|
|
895fcd4323 | ||
|
|
adefc89ab2 | ||
|
|
aec7c78fb0 | ||
|
|
10d534e836 | ||
|
|
15b2a34957 | ||
|
|
baa639a9b2 | ||
|
|
5e9b69c319 | ||
|
|
df6bb52ab0 | ||
|
|
11f8e33818 | ||
|
|
da50ac468b | ||
|
|
d5b66ff830 | ||
|
|
e8f332da43 | ||
|
|
334c6723e9 | ||
|
|
bb96b0049c | ||
|
|
0a1aa5030d | ||
|
|
be071ff82f | ||
|
|
f7b08f33ee | ||
|
|
bb69e554d0 | ||
|
|
64c257f050 | ||
|
|
b021f9aaa7 | ||
|
|
03ceb3de7f | ||
|
|
a35efb2726 | ||
|
|
58fc8e51cd | ||
|
|
0c073983e9 | ||
|
|
d03750bc53 | ||
|
|
074d314897 | ||
|
|
b039aceb21 | ||
|
|
3453746fc0 | ||
|
|
b159b061ae | ||
|
|
eb4bb601d7 | ||
|
|
92a75cf289 | ||
|
|
4c165d8107 | ||
|
|
c5165547b4 | ||
|
|
e82e1b3301 | ||
|
|
6edc219a04 | ||
|
|
597566a000 | ||
|
|
3736f0f633 | ||
|
|
5b0f05c17f | ||
|
|
614a66fc70 | ||
|
|
4c9b547e7c | ||
|
|
d9cb8dcd6f | ||
|
|
8d0e72a8bc | ||
|
|
e9d8fb95cf | ||
|
|
aa4629752a | ||
|
|
8197308cef | ||
|
|
c136e930c9 | ||
|
|
c51629b229 | ||
|
|
be3a8a82ab | ||
|
|
dfa357bbd1 | ||
|
|
08b74a0294 | ||
|
|
92af42b2c5 | ||
|
|
a3222ec496 | ||
|
|
e22a724c57 | ||
|
|
0e3688c431 | ||
|
|
31007040e5 | ||
|
|
36227215fc | ||
|
|
7bebcc066e | ||
|
|
934a047586 |
182
.agents/skills/waas-guide/SKILL.md
Normal file
182
.agents/skills/waas-guide/SKILL.md
Normal file
@@ -0,0 +1,182 @@
|
||||
---
|
||||
name: waas-guide
|
||||
description: Leitfaden fuer High-End-Webdesign (Bento-Grid), Conversion-Optimierung, DSGVO-Haertung und WaaS-B2B-Vertrieb.
|
||||
---
|
||||
|
||||
# N&D IT Solutions: WaaS-Erweiterungsstrategie
|
||||
|
||||
## SÄULE 1: Branchenspezifische Design-Strategien & Bento-Layouts
|
||||
|
||||
### 1. Lokales Premium-Handwerk (Innenausbau, Solarteure)
|
||||
* **Farbpsychologie:**
|
||||
* Hintergrund: `#0B0F19` (Corporate Dark Mode)
|
||||
* Sekundärfarbe: Warmbronze (`hsl(35, 35%, 45%)` / `#9C7A4E`)
|
||||
* Akzentfarbe: Kupfer-Glow (`hsl(20, 70%, 55%)` / `#DE6B32`)
|
||||
* Wirkung: Vermittelt Hitze, Energie, präzise Handwerksqualität und Wertigkeit.
|
||||
* **Branchenspezifische Eye-Catcher:**
|
||||
* *Tactile Craftsman UI:* Subtiles SVG-Rauschen über radialen Verläufen simuliert physikalische Oberflächen (z. B. geschliffenes Holz/Metall).
|
||||
* *Kanten-Effekt:* Lichtkanten via `border-[0.5px] border-amber-900/30` und diffuse Schatten (`box-shadow: 0 0 25px -5px rgba(222, 107, 50, 0.15)`).
|
||||
* **Bento-Layout-Struktur (3-Säulen-Grid):**
|
||||
* *Säule 1:* Box 1 (Hero-Statement, col-span-2) | Box 2 (Vorher-Nachher Slider, col-span-1)
|
||||
* *Säule 2:* Box 3 (Material-Galerie, col-span-1) | Box 4 (Rhythmusbrecher: Übergroßes, asymmetrisches Textzitat "Maßarbeit statt Masse." mit 20% Opacity Kupfer-Glow im Hintergrund, col-span-2)
|
||||
* *Säule 3:* Box 5 (Festpreis-Garantie, col-span-1) | Box 6 (Kontakt-Schnittstelle, col-span-2)
|
||||
|
||||
### 2. Medizinische Praxen & Kanzleien (Rechtsanwälte, High-End-Zahnärzte)
|
||||
* **Farbpsychologie:**
|
||||
* Hintergrund: `#0B0F19`
|
||||
* Sekundärfarbe: Tiefes Smaragdgrün (`hsl(160, 40%, 25%)` / `#265C45`)
|
||||
* Akzentfarbe: Arktis-Blau (`hsl(199, 95%, 74%)` / `#7DD3FC`)
|
||||
* Wirkung: Signalisiert beruhigende Professionalität, Hygiene, absolute Diskretion und Vertrauen.
|
||||
* **Branchenspezifische Eye-Catcher:**
|
||||
* *Liquid Glass:* Extreme Transparenz (`backdrop-blur-xl bg-slate-900/60`).
|
||||
* *Kanten-Reflexion:* Dünner, linearer Randverlauf (`border-[0.5px] border-white/10`) ohne Textur für sterile Ästhetik.
|
||||
* **Bento-Layout-Struktur (3-Säulen-Grid):**
|
||||
* *Säule 1:* Box 1 (H1 Expertise-Statement, col-span-2) | Box 2 (Direktbuchungs-Terminkalender, col-span-1)
|
||||
* *Säule 2:* Box 3 (Rhythmusbrecher: Kreisförmige Foto-Maske des Inhabers bricht eckiges Grid, col-span-1) | Box 4 (Fachgebiete & Kernkompetenzen, col-span-2)
|
||||
* *Säule 3:* Box 5 (Datenschutz-Zertifikat, col-span-1) | Box 6 (FAQ-Akkordeon, col-span-2)
|
||||
|
||||
### 3. Lokale Premium-Dienstleister (Boutique-Fitness, Agenturen)
|
||||
* **Farbpsychologie:**
|
||||
* Hintergrund: `#0B0F19`
|
||||
* Sekundärfarbe: Muted Violett (`hsl(270, 30%, 35%)` / `#533C6F`)
|
||||
* Akzentfarbe: Neon-Cyan-Glow (`hsl(180, 100%, 50%)` / `#00FFFF`)
|
||||
* Wirkung: Vermittelt Exklusivität, hohe Dynamik, Luxus und Modernität.
|
||||
* **Branchenspezifische Eye-Catcher:**
|
||||
* *Aurora Glow:* Animierte, extrem weiche Glow-Verläufe im Hintergrund (`animate-pulse-slow` mit max. 10% Deckkraft).
|
||||
* *Hover-Glow:* Kanten leuchten interaktiv bei Mausbewegung (CSS-Radialgradient-Border).
|
||||
* **Bento-Layout-Struktur (3-Säulen-Grid):**
|
||||
* *Säule 1:* Box 1 (Visual Hero, col-span-2) | Box 2 (Mitgliedschafts-Modell, col-span-1)
|
||||
* *Säule 2:* Box 3 (Kundenvorteile, col-span-1) | Box 4 (Rhythmusbrecher: Dynamisch blinkende Live-Auslastungs-Anzeige, col-span-2)
|
||||
* *Säule 3:* Box 5 (Social Proof / Testimonials, col-span-2) | Box 6 (Kontakt, col-span-1)
|
||||
|
||||
---
|
||||
|
||||
## SÄULE 2: UI/UX & Verkaufspsychologie (Conversion-Driven UX)
|
||||
|
||||
### 1. Wizard-Optimierung (Absprungrate reduzieren bei Schritt 3)
|
||||
* **Sorgenfrei-Versprechen (Mikro-Copy):** Direkt über dem E-Mail-/Telefon-Eingabefeld Platzierung von: *"Kein Spam. Kein Telefonterror. Nur Ihr fertiges Design-Konzept innerhalb von 24 Stunden."*
|
||||
* **Fortschrittserhalt:** Visuelle Anzeige, dass die Daten aus Schritt 1 & 2 bereits lokal gesichert sind und das Konzept reserviert ist.
|
||||
* **Alternative Interaktionswege:** Biete dem Nutzer neben dem Formular einen direkten WhatsApp-Short-Link an, der den Wizard-Fortschritt verschlüsselt als Text vorausfüllt.
|
||||
|
||||
### 2. Bento-Interaction-States (Barrierefrei & CLS-frei)
|
||||
* Keine Änderungen an Layout-Größen (`width`, `height`, `padding`, `border-width`), da diese Layout-Shifts (CLS) triggern.
|
||||
* Ausschließliche Nutzung von Hardware-beschleunigten CSS-Eigenschaften (`transform`, `opacity`, `box-shadow`, `border-color`).
|
||||
* Tastatur-Navigation: `:focus-visible` repliziert die Hover-Glow-Stile exakt.
|
||||
|
||||
```css
|
||||
.bento-card {
|
||||
transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 0.3s ease, border-color 0.3s ease;
|
||||
outline: none;
|
||||
}
|
||||
.bento-card:hover, .bento-card:focus-visible {
|
||||
transform: translateY(-4px) scale(1.01);
|
||||
border-color: rgba(56, 189, 248, 0.4);
|
||||
box-shadow: 0 10px 30px -10px rgba(56, 189, 248, 0.2);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. White-Space-Asymmetry (Preise im Fokus)
|
||||
* **Freiraum-Kontrast:** Die WaaS-Preise erhalten die 2,5-fache vertikale Polsterung (`py-28`) im Vergleich zu restlichen Inhaltsabschnitten (`py-12`).
|
||||
* **Asymmetrische Anordnung:** Platzierung der Preiskarten auf der linken Seite des Grids (2/3-Breite), während rechts freie, ununterbrochene Leere herrscht, die lediglich durch einen minimalen Arktis-Blau-Glow (z-0, 5% Opacity) akzentuiert wird. Dies lenkt den Blickpfad unweigerlich auf das Pricing.
|
||||
|
||||
---
|
||||
|
||||
## SÄULE 3: Datenschutz & Performance (Security-Hardening-Privacy)
|
||||
|
||||
### 1. DSGVO-Marketing-Textierung für KMUs
|
||||
```html
|
||||
<section class="py-20 bg-[#0B0F19] text-white">
|
||||
<div class="max-w-5xl mx-auto px-6">
|
||||
<span class="text-xs uppercase tracking-widest text-[#38BDF8] font-bold">100% Abmahnsicher</span>
|
||||
<h2 class="text-3xl font-extrabold mt-2 mb-6">Keine Abmahnfallen. Keine US-Server. Maximaler Datenschutz.</h2>
|
||||
<div class="grid md:grid-cols-2 gap-8">
|
||||
<div>
|
||||
<p class="text-slate-300 leading-relaxed">
|
||||
Herkömmliche Baukasten-Websites laden Schriftarten, Icons und Analysedienste unbemerkt von US-Servern (Google, Adobe etc.). Das ist illegal nach DSGVO und führt zu teuren Abmahnungen.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-slate-300 leading-relaxed">
|
||||
Unsere statische Astro-Architektur liefert <strong>alles lokal</strong> aus Ihrem eigenen Webspace aus. Keine unbemerkt geladenen Fremddienste, keine zustimmungspflichtigen Tracking-Cookies, kein Ärger. Nur reine Performance und absolute Rechtssicherheit.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
### 2. Code-Härtung (Standard-Security-Header)
|
||||
* **Content Security Policy (CSP):**
|
||||
```http
|
||||
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; upgrade-insecure-requests;
|
||||
```
|
||||
* **Sicherheitsrelevante HTTP-Response-Header:**
|
||||
* `X-Frame-Options: DENY` (Verhindert Clickjacking-Angriffe)
|
||||
* `X-Content-Type-Options: nosniff` (Verhindert MIME-Sniffing)
|
||||
* `Referrer-Policy: strict-origin-when-cross-origin`
|
||||
* `Permissions-Policy: geolocation=(), camera=(), microphone=()`
|
||||
|
||||
---
|
||||
|
||||
## SÄULE 4: B2B-Vertrieb & WaaS-Skalierung
|
||||
|
||||
### 1. WaaS-Einwandvorwegnahme (Argumentationskette)
|
||||
* **Die Leasing-Analogie:** *"Sie kaufen für Ihren Betrieb keinen Firmenwagen bar für 60.000 €, der nach vier Jahren veraltet ist. Sie leasen ihn inklusive Full-Service. WaaS ist das Leasing-Modell für Ihre Website."*
|
||||
* **Finanzielle Optimierung:** *Einmalkauf (3.000 €) blockiert Ihre Liquidität und muss über Jahre abgeschrieben werden. WaaS (99 €/Monat) ist sofort zu 100% als Betriebsausgabe steuerlich absetzbar. Hosting und Wartung sind bereits integriert (Ersparnis: ~70 €/Monat).*
|
||||
* **Die Veraltungs-Garantie:** *Websites altern rasant. Bei unserem WaaS-Modell erhalten Sie nach 24 Monaten Laufzeit einen vollständigen visuellen und technischen Relaunch Ihrer Seite – komplett kostenfrei.*
|
||||
|
||||
### 2. KI-Automatisierungs-Schnittstelle (JSON Payload für MDX-Erstellung)
|
||||
Das nachfolgende JSON-Format wird vom Onboarding-Wizard generiert und per Webhook an die KI-Pipeline gesendet, um die Astro MDX Content Collections automatisch zu strukturieren:
|
||||
|
||||
```json
|
||||
{
|
||||
"client_metadata": {
|
||||
"agency_id": "nd-it-solutions",
|
||||
"timestamp": "2026-07-18T23:31:00Z",
|
||||
"domain": "innenausbau-schmidt.de"
|
||||
},
|
||||
"branding": {
|
||||
"industry": "craftsman",
|
||||
"company_name": "Schreinerei Schmidt",
|
||||
"branding_style": "minimal-dark",
|
||||
"primary_color": "hsl(35, 35%, 45%)",
|
||||
"secondary_color": "hsl(20, 70%, 55%)"
|
||||
},
|
||||
"structure": {
|
||||
"sections": ["hero", "bento-portfolio", "roi-calculator", "gdpr-trust", "contact-form"],
|
||||
"features": {
|
||||
"before_after_slider": true,
|
||||
"online_calendar": false,
|
||||
"custom_calculator": "roi"
|
||||
}
|
||||
},
|
||||
"content_data": {
|
||||
"usps": [
|
||||
"30 Jahre Meisterbetrieb",
|
||||
"Festpreisgarantie vor Baubeginn",
|
||||
"Staubfreie Montage"
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"title": "Edler Innenausbau",
|
||||
"description": "Individuelle Raumkonzepte für Küche und Wohnbereich."
|
||||
},
|
||||
{
|
||||
"title": "Möbel nach Maß",
|
||||
"description": "Unikate aus Meisterhand, perfekt angepasst."
|
||||
}
|
||||
],
|
||||
"testimonials": [
|
||||
{
|
||||
"author": "M. Weber",
|
||||
"quote": "Beste Entscheidung! Der Umbau war absolut staubfrei."
|
||||
}
|
||||
]
|
||||
},
|
||||
"contact": {
|
||||
"phone": "+49 123 456789",
|
||||
"email": "info@innenausbau-schmidt.de",
|
||||
"address": "Hauptstraße 42, 80331 München"
|
||||
}
|
||||
}
|
||||
```
|
||||
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
.git
|
||||
.astro
|
||||
dist
|
||||
.agents
|
||||
@@ -38,16 +38,20 @@ jobs:
|
||||
|
||||
# Schreibt die Docker Compose Datei sauber ohne Einrückung
|
||||
cat << 'EOF' > docker-compose.yml
|
||||
version: '3.8'
|
||||
services:
|
||||
website:
|
||||
image: gitea.hephex.de/daniel/ndsolutionswebsite:latest
|
||||
restart: always
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "8080:4321"
|
||||
environment:
|
||||
- HOST=0.0.0.0
|
||||
- PORT=4321
|
||||
- DATA_DIR=/app/data
|
||||
volumes:
|
||||
- ./app/data:/app/data
|
||||
EOF
|
||||
|
||||
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login gitea.hephex.de -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
docker image prune -f
|
||||
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -22,3 +22,15 @@ pnpm-debug.log*
|
||||
|
||||
# jetbrains setting folder
|
||||
.idea/
|
||||
|
||||
# coverage report
|
||||
coverage/
|
||||
|
||||
# draft files
|
||||
*.draft.json
|
||||
|
||||
# test data directories
|
||||
app/test_data*/
|
||||
|
||||
|
||||
|
||||
|
||||
46
Dockerfile
46
Dockerfile
@@ -1,21 +1,41 @@
|
||||
# Build-Phase
|
||||
FROM node:20-slim AS builder
|
||||
# STAGE 1: Dependencies & Build
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install --legacy-peer-deps
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
ENV NODE_ENV=production
|
||||
RUN npm run build
|
||||
|
||||
# Production-Phase
|
||||
FROM nginx:1.25
|
||||
# STAGE 2: Production Runner
|
||||
FROM node:20-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
# Eigene Nginx-Konfiguration kopieren
|
||||
COPY default.conf /etc/nginx/conf.d/default.conf
|
||||
ENV NODE_ENV=production
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=3000
|
||||
ENV DATA_DIR=/app/data
|
||||
|
||||
# Nginx konfigurieren für non-root Port 8080
|
||||
RUN touch /var/run/nginx.pid && \
|
||||
chown -R nginx:nginx /var/run/nginx.pid /var/cache/nginx /var/log/nginx /etc/nginx/conf.d
|
||||
# Ordner anlegen & Schreibrechte vorbereiten
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
USER nginx
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
EXPOSE 8080
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY docker-entrypoint.sh /app/docker-entrypoint.sh
|
||||
|
||||
# Ausführungsrechte für das Entrypoint-Skript setzen
|
||||
RUN chmod +x /app/docker-entrypoint.sh
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
# Healthcheck für Docker / Nginx Proxy
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1
|
||||
|
||||
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
||||
CMD ["node", "./dist/server/entry.mjs"]
|
||||
|
||||
32
app/data/site.config.json
Normal file
32
app/data/site.config.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"siteName": "N&D IT Solutions",
|
||||
"metaDescription": "High-Performance Webdesign & Digital Solutions - 100% DSGVO-konform.",
|
||||
"is_demo_mode": false,
|
||||
"header": {
|
||||
"brandName": "N&D",
|
||||
"brandHighlight": "i-t SOLUTIONS",
|
||||
"ctaText": "Erstgespräch buchen",
|
||||
"ctaLink": "#contact",
|
||||
"showThemeToggle": true,
|
||||
"glassEffect": true
|
||||
},
|
||||
"footer": {
|
||||
"brandDescription": "Ihr Partner für professionelles Webdesign, Website-Entwicklung und lokale SEO.",
|
||||
"copyrightText": "N&D i-t SOLUTIONS. Alle Rechte vorbehalten.",
|
||||
"showLegalLinks": true
|
||||
},
|
||||
"site_info": {
|
||||
"title": "N&D IT Solutions",
|
||||
"homepage_id": "page_home"
|
||||
},
|
||||
"pages": [
|
||||
{
|
||||
"id": "page_home",
|
||||
"slug": "/",
|
||||
"title": "Startseite",
|
||||
"is_published": true,
|
||||
"sections": []
|
||||
}
|
||||
],
|
||||
"updated_at": "2026-08-09T22:28:31.200Z"
|
||||
}
|
||||
1
app/data/site.desktop.config.json
Normal file
1
app/data/site.desktop.config.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
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
|
||||
}
|
||||
9
app/data/theme.config.json
Normal file
9
app/data/theme.config.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"activePreset": "midnightBlue",
|
||||
"customColors": {
|
||||
"bg": "#0b0f19",
|
||||
"accent": "#38bdf8",
|
||||
"text": "#e0e0e0",
|
||||
"border": ""
|
||||
}
|
||||
}
|
||||
BIN
app/data/waas.db
Normal file
BIN
app/data/waas.db
Normal file
Binary file not shown.
@@ -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()]
|
||||
});
|
||||
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"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,27 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
website:
|
||||
image: gitea.hephex.de/daniel/websitedummy:latest
|
||||
container_name: websitedummy_app
|
||||
app:
|
||||
image: nd-waas-engine:latest
|
||||
container_name: waas_${CUSTOMER_SLUG:-kunden_app}
|
||||
restart: always
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- DATA_DIR=/app/data
|
||||
volumes:
|
||||
# Hier werden site.config.json, sqlite.db, uploads etc. automatisch vom Container angelegt
|
||||
- ./data:/app/data
|
||||
ports:
|
||||
- "8080:8080"
|
||||
# Anbindung an deinen händischen Nginx Reverse Proxy auf dem Host
|
||||
- "127.0.0.1:${PORT:-8080}:3000"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "http://localhost:3000/api/health"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
78
docker-entrypoint.sh
Normal file
78
docker-entrypoint.sh
Normal file
@@ -0,0 +1,78 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
DATA_DIR=${DATA_DIR:-/app/data}
|
||||
|
||||
echo "🚀 [WaaS Container Entrypoint] Prüfe Daten-Volume unter ${DATA_DIR}..."
|
||||
|
||||
# 1. Sicherstellen, dass das Volume-Verzeichnis existiert
|
||||
mkdir -p "$DATA_DIR"
|
||||
|
||||
# 1.5 SQLite DB initialisieren (falls nicht vorhanden)
|
||||
if [ ! -f "$DATA_DIR/waas.db" ]; then
|
||||
echo "🗄️ Keine SQLite-Datenbank waas.db gefunden. Initialisierung wird beim Anwendungsstart durchgeführt..."
|
||||
fi
|
||||
|
||||
# 2. site.config.json Seeding
|
||||
if [ ! -f "$DATA_DIR/site.config.json" ]; then
|
||||
echo "📄 Keine site.config.json gefunden. Erstelle Standard-Konfiguration..."
|
||||
cat <<EOF > "$DATA_DIR/site.config.json"
|
||||
{
|
||||
"site_info": {
|
||||
"title": "N&D IT Solutions Kunden-Website",
|
||||
"company_name": "Mein Unternehmen",
|
||||
"homepage_id": "page_home_01"
|
||||
},
|
||||
"navigation": [
|
||||
{ "label": "Startseite", "page_id": "page_home_01" }
|
||||
],
|
||||
"pages": [
|
||||
{
|
||||
"id": "page_home_01",
|
||||
"slug": "/",
|
||||
"title": "Startseite",
|
||||
"is_published": true,
|
||||
"sections": [
|
||||
{
|
||||
"id": "sec_hero_init",
|
||||
"type": "HeroSection",
|
||||
"settings": {
|
||||
"title": "Herzlich Willkommen",
|
||||
"subtitle": "Ihre neue Website ist erfolgreich gestartet."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
# 3. theme.config.json Seeding
|
||||
if [ ! -f "$DATA_DIR/theme.config.json" ]; then
|
||||
echo "🎨 Keine theme.config.json gefunden. Erstelle Standard-Theme..."
|
||||
cat <<EOF > "$DATA_DIR/theme.config.json"
|
||||
{
|
||||
"presetKey": "corporate-dark",
|
||||
"mode": "dark"
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
# 4. smtp.config.json Seeding
|
||||
if [ ! -f "$DATA_DIR/smtp.config.json" ]; then
|
||||
echo "✉️ Keine smtp.config.json gefunden. Erstelle Standard-SMTP Config..."
|
||||
cat <<EOF > "$DATA_DIR/smtp.config.json"
|
||||
{
|
||||
"host": "localhost",
|
||||
"port": 1025,
|
||||
"from": "noreply@kunden-domain.de",
|
||||
"recipient": "info@kunden-domain.de"
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
echo "✅ All-in-One Initialisierung abgeschlossen. Starte Astro SSR Node Server..."
|
||||
|
||||
# Führe den eigentlichen Befehl aus (CMD aus Dockerfile)
|
||||
exec "$@"
|
||||
16
nginx_waas_site.conf
Normal file
16
nginx_waas_site.conf
Normal file
@@ -0,0 +1,16 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name spielwiese.hephex.de;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
2939
package-lock.json
generated
2939
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@@ -12,16 +12,24 @@
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^8.3.4",
|
||||
"@astrojs/react": "^3.6.3",
|
||||
"@astrojs/tailwind": "^5.1.4",
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@fontsource/space-grotesk": "^5.2.10",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"astro": "^4.16.0",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"nodemailer": "^9.0.5",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^5.7.0",
|
||||
"tailwindcss": "^3.4.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/nodemailer": "^8.0.1",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@vitest/coverage-v8": "^4.1.10",
|
||||
"vitest": "^4.1.10"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
65
public/js/editor-bridge.js
Normal file
65
public/js/editor-bridge.js
Normal file
@@ -0,0 +1,65 @@
|
||||
// public/js/editor-bridge.js
|
||||
(function () {
|
||||
// Bridge nur im Preview-Modus aktivieren
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.get('preview') !== 'draft') return;
|
||||
|
||||
console.log('[N&D Editor Bridge] Active on preview canvas');
|
||||
|
||||
// Optischer Hinweis-Banner im Bearbeitungsmodus
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const banner = document.createElement('div');
|
||||
banner.id = 'editor-preview-banner';
|
||||
banner.style.cssText = 'position: fixed; top: 0; left: 0; right: 0; z-index: 99999; background: #d97706; color: #0f172a; text-align: center; padding: 6px 12px; font-size: 12px; font-weight: 700; font-family: monospace; letter-spacing: 0.5px; box-shadow: 0 2px 10px rgba(0,0,0,0.3); pointer-events: none; opacity: 0.95;';
|
||||
banner.innerHTML = '🛠️ BEARBEITUNGSMODUS (VORSCHAU / ENTWURF) — Änderungen sind noch nicht live!';
|
||||
document.body.appendChild(banner);
|
||||
const currentPadding = parseInt(document.body.style.paddingTop || '0', 10);
|
||||
document.body.style.paddingTop = (currentPadding + 30) + 'px';
|
||||
});
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
const { type, payload } = event.data || {};
|
||||
|
||||
if (type === 'THEME_UPDATE') {
|
||||
const root = document.documentElement;
|
||||
if (payload.colors) {
|
||||
Object.entries(payload.colors).forEach(([key, val]) => {
|
||||
root.style.setProperty(`--${key}`, val);
|
||||
});
|
||||
}
|
||||
|
||||
// Live Section DOM Patching
|
||||
if (payload.sectionId && payload.settings) {
|
||||
const secElement = document.querySelector(`[data-section-id="${payload.sectionId}"], #${payload.sectionId}`) || document.querySelector('section');
|
||||
if (secElement) {
|
||||
const { title, subtitle, cta_text, bg_color, accent_color } = payload.settings;
|
||||
|
||||
if (title !== undefined) {
|
||||
const h1OrH2 = secElement.querySelector('h1, h2, h3');
|
||||
if (h1OrH2) h1OrH2.textContent = title;
|
||||
}
|
||||
if (subtitle !== undefined) {
|
||||
const p = secElement.querySelector('p');
|
||||
if (p) p.textContent = subtitle;
|
||||
}
|
||||
if (cta_text !== undefined) {
|
||||
const btn = secElement.querySelector('a, button');
|
||||
if (btn) btn.textContent = cta_text;
|
||||
}
|
||||
if (bg_color !== undefined && bg_color !== '') {
|
||||
secElement.style.backgroundColor = bg_color;
|
||||
}
|
||||
if (accent_color !== undefined && accent_color !== '') {
|
||||
secElement.style.setProperty('--accent-color', accent_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'RELOAD_CANVAS') {
|
||||
// Sanfter Reload des Iframes bei Strukturänderungen
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -13,15 +13,15 @@ import { FaEye, FaRocket, FaHandshake } from 'react-icons/fa';
|
||||
|
||||
<!-- Text/Profile Column -->
|
||||
<div class="fade-in-section opacity-0 translate-y-8 transition-all duration-1000 ease-out">
|
||||
<h2 class="text-xs font-bold tracking-widest text-brand-cyan uppercase mb-3">Unternehmensprofil</h2>
|
||||
<h2 class="text-xs font-bold tracking-widest text-brand-cyan uppercase mb-3">Über uns</h2>
|
||||
<h3 class="text-4xl md:text-5xl font-bold text-brand-primary dark:text-white mb-6">
|
||||
Wir machen Webdesign einfach und bezahlbar für Kleinbetriebe.
|
||||
Wir machen Webseiten einfach und bezahlbar für Betriebe wie Ihren.
|
||||
</h3>
|
||||
<p class="text-gray-600 dark:text-gray-300 text-lg leading-relaxed mb-6 font-light">
|
||||
N&D i-t SOLUTIONS wurde gegründet, um kleinen Unternehmen, Handwerkern und Dienstleistern eine professionelle Online-Präsenz zu ermöglichen. Wir bieten erstklassige Websites ohne die unerschwinglichen Kosten großer Agenturen.
|
||||
Wir haben N&D gegründet, um kleinen Unternehmen, Geschäften und Handwerkern zu helfen. Viele haben keine Lust auf komplizierte Technik oder extrem teure Agenturen. Bei uns bekommen Sie eine moderne Webseite – ganz ohne Kopfschmerzen.
|
||||
</p>
|
||||
<p class="text-gray-600 dark:text-gray-300 text-lg leading-relaxed mb-8 font-light">
|
||||
Unser Fokus liegt auf schnellen Ladezeiten, optimaler Bedienung auf dem Smartphone (Mobile First) und lokaler Auffindbarkeit bei Google.
|
||||
Wir sorgen dafür, dass Ihre Seite auf dem Handy richtig gut aussieht und schnell lädt. Und vor allem: dass Ihre Kunden Sie bei Google auch wirklich finden.
|
||||
</p>
|
||||
|
||||
<!-- Fun facts -->
|
||||
@@ -48,12 +48,12 @@ import { FaEye, FaRocket, FaHandshake } from 'react-icons/fa';
|
||||
<div class="fade-in-section opacity-0 translate-y-8 transition-all duration-1000 ease-out delay-150 p-8 rounded-3xl bg-white/30 dark:bg-brand-primary/40 border border-white/20 dark:border-white/5 backdrop-blur-md shadow-lg hover:shadow-xl transition-shadow duration-300">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="p-3 rounded-2xl bg-brand-cyan/10 text-brand-cyan">
|
||||
<FaRocket class="w-6 h-6" />
|
||||
<FaRocket className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-xl font-bold text-brand-primary dark:text-white mb-2">Unsere Mission</h4>
|
||||
<h4 class="text-xl font-bold text-brand-primary dark:text-white mb-2">Unser Ziel</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 leading-relaxed font-light">
|
||||
Wir verhelfen lokalen Betrieben zu einer erstklassigen Online-Präsenz, die neue Kunden anzieht und Ihre Marke perfekt widerspiegelt.
|
||||
Wir bringen Ihren Betrieb ins Internet – so schick und professionell, dass Kunden Sie gerne besuchen und direkt anrufen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,12 +63,12 @@ import { FaEye, FaRocket, FaHandshake } from 'react-icons/fa';
|
||||
<div class="fade-in-section opacity-0 translate-y-8 transition-all duration-1000 ease-out delay-300 p-8 rounded-3xl bg-white/30 dark:bg-brand-primary/40 border border-white/20 dark:border-white/5 backdrop-blur-md shadow-lg hover:shadow-xl transition-shadow duration-300">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="p-3 rounded-2xl bg-brand-magenta/10 text-brand-magenta">
|
||||
<FaEye class="w-6 h-6" />
|
||||
<FaEye className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-xl font-bold text-brand-primary dark:text-white mb-2">Unsere Vision</h4>
|
||||
<h4 class="text-xl font-bold text-brand-primary dark:text-white mb-2">Gleiche Chancen für alle</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 leading-relaxed font-light">
|
||||
Eine digitale Landschaft, in der auch kleine Händler und lokale Handwerker die gleichen digitalen Chancen haben wie Großkonzerne.
|
||||
Egal ob Dönerladen, Friseursalon oder Handwerker: Jeder verdient eine Webseite, die neue Kunden bringt.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -78,12 +78,12 @@ import { FaEye, FaRocket, FaHandshake } from 'react-icons/fa';
|
||||
<div class="fade-in-section opacity-0 translate-y-8 transition-all duration-1000 ease-out delay-450 p-8 rounded-3xl bg-white/30 dark:bg-brand-primary/40 border border-white/20 dark:border-white/5 backdrop-blur-md shadow-lg hover:shadow-xl transition-shadow duration-300">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="p-3 rounded-2xl bg-brand-cyan/10 text-brand-cyan">
|
||||
<FaHandshake class="w-6 h-6" />
|
||||
<FaHandshake className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-xl font-bold text-brand-primary dark:text-white mb-2">Unser Versprechen</h4>
|
||||
<h4 class="text-xl font-bold text-brand-primary dark:text-white mb-2">Ehrliches Versprechen</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 leading-relaxed font-light">
|
||||
Keine versteckten monatlichen Gebühren oder Bindungen. Ihre Website gehört zu 100% Ihnen – mit ehrlichem Support auf Augenhöhe.
|
||||
Kein Kleingedrucktes, keine versteckten Kosten. Wir sprechen auf Augenhöhe mit Ihnen – ganz ohne Fachbegriffe.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,14 +10,14 @@ import { FaTimes, FaEnvelope, FaUser, FaBuilding, FaCheckCircle, FaSpinner } fro
|
||||
|
||||
<!-- Close Button -->
|
||||
<button id="close-contact-modal" class="absolute top-6 right-6 p-2 rounded-full bg-gray-100 hover:bg-gray-200 dark:bg-white/5 dark:hover:bg-white/10 text-gray-500 dark:text-gray-400 hover:text-brand-primary dark:hover:text-white transition-all focus:outline-none" aria-label="Modal schließen">
|
||||
<FaTimes class="w-5 h-5" />
|
||||
<FaTimes className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-3xl font-bold text-brand-primary dark:text-white mb-2">Projekt anfragen</h3>
|
||||
<h3 class="text-3xl font-bold text-brand-primary dark:text-white mb-2">Kontakt aufnehmen</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400 text-sm">
|
||||
Erzählen Sie uns von Ihrem Vorhaben. Wir melden uns innerhalb von 24 Stunden.
|
||||
Schreiben Sie uns einfach eine kurze Nachricht. Wir melden uns innerhalb von 24 Stunden bei Ihnen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -26,16 +26,16 @@ import { FaTimes, FaEnvelope, FaUser, FaBuilding, FaCheckCircle, FaSpinner } fro
|
||||
|
||||
<!-- Name Field -->
|
||||
<div>
|
||||
<label for="name" class="block text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">Name *</label>
|
||||
<label for="name" class="block text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">Ihr Name *</label>
|
||||
<div class="relative">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 text-gray-400">
|
||||
<FaUser class="w-4 h-4" />
|
||||
<FaUser className="w-4 h-4" />
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
placeholder="Ihr vollständiger Name"
|
||||
placeholder="Ihr Name"
|
||||
class="w-full pl-11 pr-4 py-3 bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl focus:border-brand-cyan focus:outline-none focus:ring-1 focus:ring-brand-cyan transition-all text-brand-primary dark:text-white"
|
||||
required
|
||||
/>
|
||||
@@ -45,16 +45,16 @@ import { FaTimes, FaEnvelope, FaUser, FaBuilding, FaCheckCircle, FaSpinner } fro
|
||||
|
||||
<!-- Firma Field -->
|
||||
<div>
|
||||
<label for="company" class="block text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">Firma / Organisation</label>
|
||||
<label for="company" class="block text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">Ihr Laden / Ihr Betrieb</label>
|
||||
<div class="relative">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 text-gray-400">
|
||||
<FaBuilding class="w-4 h-4" />
|
||||
<FaBuilding className="w-4 h-4" />
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
id="company"
|
||||
name="company"
|
||||
placeholder="Name Ihres Unternehmens"
|
||||
placeholder="z.B. Dönereck"
|
||||
class="w-full pl-11 pr-4 py-3 bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl focus:border-brand-cyan focus:outline-none focus:ring-1 focus:ring-brand-cyan transition-all text-brand-primary dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
@@ -62,10 +62,10 @@ import { FaTimes, FaEnvelope, FaUser, FaBuilding, FaCheckCircle, FaSpinner } fro
|
||||
|
||||
<!-- Email Field -->
|
||||
<div>
|
||||
<label for="email" class="block text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">E-Mail-Adresse *</label>
|
||||
<label for="email" class="block text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">Ihre E-Mail-Adresse *</label>
|
||||
<div class="relative">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 text-gray-400">
|
||||
<FaEnvelope class="w-4 h-4" />
|
||||
<FaEnvelope className="w-4 h-4" />
|
||||
</span>
|
||||
<input
|
||||
type="email"
|
||||
@@ -86,7 +86,7 @@ import { FaTimes, FaEnvelope, FaUser, FaBuilding, FaCheckCircle, FaSpinner } fro
|
||||
id="message"
|
||||
name="message"
|
||||
rows="4"
|
||||
placeholder="Beschreiben Sie kurz Ihr Projekt..."
|
||||
placeholder="Schreiben Sie kurz, was Sie brauchen (z.B. 'Ich brauche eine neue Webseite für meine Pizzeria...')"
|
||||
class="w-full p-4 bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl focus:border-brand-cyan focus:outline-none focus:ring-1 focus:ring-brand-cyan transition-all text-brand-primary dark:text-white resize-none"
|
||||
required
|
||||
></textarea>
|
||||
@@ -103,7 +103,7 @@ import { FaTimes, FaEnvelope, FaUser, FaBuilding, FaCheckCircle, FaSpinner } fro
|
||||
required
|
||||
/>
|
||||
<label for="privacy" class="text-xs text-gray-500 dark:text-gray-400 leading-normal">
|
||||
Ich stimme der Verarbeitung meiner Daten gemäß der <a href="/privacy" target="_blank" class="text-brand-cyan hover:underline">Datenschutzerklärung</a> zu. *
|
||||
Ich bin mit der <a href="/privacy" target="_blank" class="text-brand-cyan hover:underline">Datenschutzerklärung</a> einverstanden. *
|
||||
</label>
|
||||
</div>
|
||||
<p id="privacy-error" class="error-msg text-xs text-red-500 hidden">Sie müssen der Datenschutzerklärung zustimmen.</p>
|
||||
@@ -114,8 +114,8 @@ import { FaTimes, FaEnvelope, FaUser, FaBuilding, FaCheckCircle, FaSpinner } fro
|
||||
id="submit-btn"
|
||||
class="w-full py-4 mt-2 inline-flex items-center justify-center gap-2 bg-gradient-to-r from-brand-cyan to-brand-magenta text-brand-primary font-bold rounded-xl shadow-lg transition-all duration-300 hover:scale-[1.02] focus:outline-none hover:shadow-[0_0_20px_rgba(0,229,255,0.4)] relative overflow-hidden group active:scale-95"
|
||||
>
|
||||
<span id="btn-text">Anfrage senden</span>
|
||||
<span id="btn-loader" class="hidden"><FaSpinner class="w-5 h-5 animate-spin" /></span>
|
||||
<span id="btn-text">Nachricht senden</span>
|
||||
<span id="btn-loader" class="hidden"><FaSpinner className="w-5 h-5 animate-spin" /></span>
|
||||
</button>
|
||||
|
||||
</form>
|
||||
@@ -126,7 +126,7 @@ import { FaTimes, FaEnvelope, FaUser, FaBuilding, FaCheckCircle, FaSpinner } fro
|
||||
<div id="success-modal" class="fixed inset-0 z-[120] hidden items-center justify-center p-4 bg-brand-primary/95 backdrop-blur-lg transition-all duration-300">
|
||||
<div class="max-w-md w-full bg-white dark:bg-brand-primary border border-gray-200 dark:border-white/10 p-8 rounded-3xl shadow-2xl text-center transform scale-95 opacity-0 transition-all duration-300 ease-out" id="success-box">
|
||||
<div class="text-brand-cyan mb-6 flex justify-center">
|
||||
<FaCheckCircle class="w-16 h-16 animate-bounce" />
|
||||
<FaCheckCircle className="w-16 h-16 animate-bounce" />
|
||||
</div>
|
||||
<h3 class="text-3xl font-bold text-brand-primary dark:text-white mb-4">Nachricht gesendet!</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400 text-sm leading-relaxed mb-8">
|
||||
|
||||
150
src/components/ContactSection.astro
Normal file
150
src/components/ContactSection.astro
Normal file
@@ -0,0 +1,150 @@
|
||||
---
|
||||
// src/components/ContactSection.astro
|
||||
interface Props {
|
||||
id?: string;
|
||||
settings?: {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
submit_button_text?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const { id = 'contact', settings = {} } = Astro.props;
|
||||
const title = settings.title || 'Kontaktieren Sie uns';
|
||||
const subtitle = settings.subtitle || 'Schreiben Sie uns eine Nachricht. Wir melden uns innerhalb von 24 Stunden.';
|
||||
const buttonText = settings.submit_button_text || 'Nachricht absenden';
|
||||
---
|
||||
|
||||
<section id={id} class="py-16 px-4 max-w-4xl mx-auto">
|
||||
<div class="preset-card p-8 bg-slate-900/80 border border-white/10 rounded-2xl shadow-2xl backdrop-blur-md">
|
||||
<div class="text-center mb-8">
|
||||
<h2 class="text-2xl font-bold text-white tracking-tight">{title}</h2>
|
||||
<p class="text-slate-400 text-sm mt-2">{subtitle}</p>
|
||||
</div>
|
||||
|
||||
<div id="form-feedback" role="status" aria-live="polite" class="hidden mb-6 p-4 rounded-lg text-sm font-semibold"></div>
|
||||
|
||||
<form id="contact-form" class="space-y-6" novalidate>
|
||||
<div class="hidden" aria-hidden="true">
|
||||
<label for="website_hp">Bitte dieses Feld leer lassen</label>
|
||||
<input type="text" id="website_hp" name="website_hp" tabindex="-1" autocomplete="off" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="sender_name" class="block text-xs font-semibold text-slate-300 uppercase tracking-wider mb-2">
|
||||
Ihr Name <span class="text-sky-400" aria-hidden="true">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="sender_name"
|
||||
name="sender_name"
|
||||
required
|
||||
aria-required="true"
|
||||
placeholder="Max Mustermann"
|
||||
class="w-full bg-slate-950/60 border border-slate-800 rounded-lg p-3 text-sm text-white placeholder-slate-600 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-transparent transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="sender_email" class="block text-xs font-semibold text-slate-300 uppercase tracking-wider mb-2">
|
||||
E-Mail Adresse <span class="text-sky-400" aria-hidden="true">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="sender_email"
|
||||
name="sender_email"
|
||||
required
|
||||
aria-required="true"
|
||||
placeholder="max@beispiel.de"
|
||||
class="w-full bg-slate-950/60 border border-slate-800 rounded-lg p-3 text-sm text-white placeholder-slate-600 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-transparent transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="message_text" class="block text-xs font-semibold text-slate-300 uppercase tracking-wider mb-2">
|
||||
Ihre Nachricht <span class="text-sky-400" aria-hidden="true">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="message_text"
|
||||
name="message_text"
|
||||
rows="5"
|
||||
required
|
||||
aria-required="true"
|
||||
placeholder="Wie können wir Ihnen helfen?"
|
||||
class="w-full bg-slate-950/60 border border-slate-800 rounded-lg p-3 text-sm text-white placeholder-slate-600 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-transparent transition-all resize-y"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="privacy_consent"
|
||||
name="privacy_consent"
|
||||
required
|
||||
aria-required="true"
|
||||
class="mt-1 h-4 w-4 accent-sky-500 rounded border-slate-800 bg-slate-950/60 cursor-pointer focus:ring-2 focus:ring-sky-500"
|
||||
/>
|
||||
<label for="privacy_consent" class="text-xs text-slate-400 leading-relaxed cursor-pointer">
|
||||
Ich stimme der Verarbeitung meiner Angaben gemäß der <a href="/datenschutz" class="text-sky-400 underline hover:text-sky-300">Datenschutzerklärung</a> zur Kontaktaufnahme zu. <span class="text-sky-400" aria-hidden="true">*</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
id="btn-submit-contact"
|
||||
class="w-full bg-sky-500 hover:bg-sky-400 text-slate-950 font-bold py-3.5 px-6 rounded-lg text-sm transition-all shadow-lg shadow-sky-500/20 focus:outline-none focus:ring-2 focus:ring-sky-300"
|
||||
>
|
||||
{buttonText}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
const form = document.getElementById('contact-form') as HTMLFormElement;
|
||||
const feedback = document.getElementById('form-feedback');
|
||||
const submitBtn = document.getElementById('btn-submit-contact') as HTMLButtonElement;
|
||||
|
||||
form?.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
if (!feedback || !submitBtn) return;
|
||||
|
||||
// Client-side Honeypot Check
|
||||
const hp = (document.getElementById('website_hp') as HTMLInputElement)?.value;
|
||||
if (hp) return; // Stiller Abbruch bei Bot-Befüllung
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = 'Wird gesendet...';
|
||||
|
||||
const formData = new FormData(form);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/contact/send', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
feedback.classList.remove('hidden', 'bg-red-950/80', 'text-red-300', 'border-red-800', 'bg-emerald-950/80', 'text-emerald-300', 'border-emerald-800');
|
||||
|
||||
if (res.ok) {
|
||||
feedback.classList.add('bg-emerald-950/80', 'text-emerald-300', 'border', 'border-emerald-800');
|
||||
feedback.textContent = 'Vielen Dank! Ihre Nachricht wurde erfolgreich übermittelt.';
|
||||
form.reset();
|
||||
} else {
|
||||
throw new Error(result.error || 'Fehler beim Senden.');
|
||||
}
|
||||
} catch (err: any) {
|
||||
feedback.classList.add('bg-red-950/80', 'text-red-300', 'border', 'border-red-800');
|
||||
feedback.textContent = err.message || 'Nachricht konnte nicht gesendet werden. Bitte versuchen Sie es später erneut.';
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = 'Nachricht absenden';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
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>
|
||||
---
|
||||
@@ -1,5 +1,13 @@
|
||||
---
|
||||
// src/components/Footer.astro
|
||||
import { loadWaasConfigs } from '../utils/configLoader';
|
||||
|
||||
const { siteConfig } = loadWaasConfigs(Astro.request.headers.get('user-agent') ?? '');
|
||||
const footer = siteConfig.footer || {
|
||||
brandDescription: 'Ihr Partner für professionelles Webdesign, Website-Entwicklung und lokale SEO für Kleinbetriebe & KMU.',
|
||||
copyrightText: 'N&D i-t SOLUTIONS. Alle Rechte vorbehalten.',
|
||||
showLegalLinks: true
|
||||
};
|
||||
const currentYear = new Date().getFullYear();
|
||||
---
|
||||
|
||||
@@ -10,10 +18,11 @@ const currentYear = new Date().getFullYear();
|
||||
<!-- Brand logo/name -->
|
||||
<div>
|
||||
<a href="#" class="text-lg font-bold tracking-wider text-slate-900 dark:text-white">
|
||||
N&D <span class="text-brand-arcticBlue">i-t SOLUTIONS</span>
|
||||
{siteConfig.header?.brandName || 'N&D'} <span class="text-accent">{siteConfig.header?.brandHighlight || 'i-t SOLUTIONS'}</span>
|
||||
</a>
|
||||
|
||||
<p class="mt-2 text-xs text-slate-500 dark:text-gray-500 max-w-sm">
|
||||
Ihr Partner für professionelles Webdesign, Website-Entwicklung und lokale SEO für Kleinbetriebe & KMU.
|
||||
{footer.brandDescription}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -31,13 +40,15 @@ const currentYear = new Date().getFullYear();
|
||||
<!-- Bottom Copyright & Legal Links -->
|
||||
<div class="flex flex-col sm:flex-row items-center justify-between gap-4 text-xs text-slate-500 dark:text-gray-500">
|
||||
<div>
|
||||
© {currentYear} N&D i-t SOLUTIONS. Alle Rechte vorbehalten.
|
||||
</div>
|
||||
<div class="flex gap-6">
|
||||
<a href="/privacy" class="hover:text-slate-900 dark:hover:text-white transition-colors">Datenschutz</a>
|
||||
<span class="text-slate-300 dark:text-white/10">|</span>
|
||||
<a href="/privacy#impressum" class="hover:text-slate-900 dark:hover:text-white transition-colors">Impressum</a>
|
||||
© {currentYear} {footer.copyrightText}
|
||||
</div>
|
||||
{footer.showLegalLinks !== false && (
|
||||
<div class="flex gap-6">
|
||||
<a href="/privacy" class="hover:text-slate-900 dark:hover:text-white transition-colors">Datenschutz</a>
|
||||
<span class="text-slate-300 dark:text-white/10">|</span>
|
||||
<a href="/privacy#impressum" class="hover:text-slate-900 dark:hover:text-white transition-colors">Impressum</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
---
|
||||
// src/components/HeroAndLiquidGlass.astro
|
||||
|
||||
const heroTemplates = [
|
||||
{ id: "01", name: "Der Handwerker-Klassiker", style: "Einfach & Bodenständig", layout: "boxed", design: "Brutalismus", colors: "from-[#FDFBF7] to-[#FAF7F2] text-[#2C1A11] border-[#2C1A11]/10" },
|
||||
{ id: "02", name: "Dachdecker Meister", style: "Elegant & Professionell", layout: "split", design: "Glassmorphismus", colors: "from-zinc-900 to-zinc-950 text-white border-yellow-500/20" },
|
||||
{ id: "03", name: "Solar & Photovoltaik", style: "Tech & Zukunft", layout: "bento", design: "SaaS-Minimalismus", colors: "from-zinc-900 to-black text-zinc-100 border-orange-500/20" },
|
||||
{ id: "04", name: "Die Kanzlei", style: "Seriös & Clean", layout: "asymmetric", design: "Glassmorphismus", colors: "from-[#0B0F19] to-[#070A13] text-white border-emerald-500/20" },
|
||||
{ id: "05", name: "Bio-Hofladen", style: "Rustikal & Natur", layout: "single", design: "Neomorphismus", colors: "from-[#FAF7F2] to-[#E8DCC4] text-[#4A3B32] border-[#E8DCC4]" },
|
||||
{ id: "06", name: "Sanitär & Heizung", style: "Klar & Zuverlässig", layout: "bento", design: "SaaS-Minimalismus", colors: "from-white to-slate-50 text-slate-800 border-slate-200" },
|
||||
{ id: "07", name: "KFZ Schnellservice", style: "Dynamisch & Direkt", layout: "split", design: "Brutalismus", colors: "from-zinc-800 to-zinc-900 text-white border-zinc-700" },
|
||||
{ id: "08", name: "Schreinerei Holzart", style: "Handwerk & Präzision", layout: "boxed", design: "SaaS-Minimalismus", colors: "from-[#F5F2EB] to-[#D9D2C5] text-[#3D3A36] border-[#D9D2C5]" },
|
||||
{ id: "09", name: "Steuerberater Premium", style: "Exklusiv & Diskret", layout: "bento", design: "Glassmorphismus", colors: "from-slate-900 to-slate-950 text-slate-100 border-amber-500/20" },
|
||||
{ id: "10", name: "Garten- & Landschaftsbau", style: "Frisch & Modern", layout: "asymmetric", design: "Glassmorphismus", colors: "from-[#F3F7F4] to-white text-slate-800 border-emerald-200" }
|
||||
];
|
||||
---
|
||||
|
||||
<!-- Hero-Sektion: Hybrid Liquid Glass Style & Aurora Glow -->
|
||||
<section class="relative pt-36 pb-24 overflow-hidden bg-slate-50 dark:bg-[#070A13] transition-colors duration-300">
|
||||
<section class="relative pt-36 pb-24 overflow-hidden bg-slate-50 dark:bg-[#0B0F19] transition-colors duration-300">
|
||||
<!-- Weiche, pastellfarbene Aurora Glows im Hintergrund (Light/Dark angepasst) -->
|
||||
<div class="absolute top-[-10%] left-[-10%] w-[50%] h-[50%] bg-sky-200/50 dark:bg-sky-500/10 blur-[130px] rounded-full pointer-events-none transition-colors duration-300 animate-drift"></div>
|
||||
<div class="absolute bottom-[-10%] right-[-10%] w-[60%] h-[60%] bg-indigo-200/40 dark:bg-indigo-500/10 blur-[150px] rounded-full pointer-events-none transition-colors duration-300 animate-drift-delayed"></div>
|
||||
@@ -18,23 +31,23 @@
|
||||
<div class="lg:col-span-7 space-y-8">
|
||||
|
||||
<h1 class="text-4xl sm:text-7xl font-extrabold tracking-tight text-slate-900 dark:text-white leading-[1.1] transition-colors duration-300">
|
||||
Sorgenfreies Hosting <br/>
|
||||
Ihre eigene Webseite. <br/>
|
||||
<span class="text-sky-600 dark:text-sky-400">
|
||||
für Ihre Website.
|
||||
Fix und fertig gemacht.
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p class="text-slate-600 dark:text-zinc-400 text-sm sm:text-base max-w-xl leading-relaxed transition-colors duration-300">
|
||||
Wir hosten, sichern und pflegen Ihren Internetauftritt auf deutschen Servern. Im monatlichen Sorgenfrei-Paket ist alles inklusive: tägliche Backups, DSGVO-Updates, SSL-Verschlüsselung und auf Wunsch die Erstellung Ihrer Website.
|
||||
Wir bauen, pflegen und sichern Ihren Internetauftritt. Sie müssen sich um absolut nichts kümmern. Für einen fairen monatlichen Festpreis ist alles drin: Gestaltung, Technik, Schutz vor Abmahnungen und schnelle Hilfe bei Änderungen.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-wrap gap-4 pt-2">
|
||||
<a href="#contact" class="inline-flex items-center justify-center gap-2 px-6 py-3.5 bg-sky-600 hover:bg-sky-700 dark:bg-sky-500 dark:hover:bg-white text-white dark:text-zinc-950 font-bold text-xs uppercase tracking-wider rounded transition-all duration-300 shadow-[0_4px_20px_rgba(2,132,199,0.2)] dark:shadow-[0_0_20px_rgba(56,189,248,0.2)]">
|
||||
Erstgespräch buchen
|
||||
Kostenloses Gespräch buchen
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" class="w-3 h-3 fill-current" aria-hidden="true"><path d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"/></svg>
|
||||
</a>
|
||||
<a href="/templates" class="inline-flex items-center justify-center gap-2 px-6 py-3.5 bg-white hover:bg-slate-50 dark:bg-slate-900/60 dark:hover:bg-slate-800 border border-slate-200 dark:border-white/10 text-slate-700 dark:text-white font-bold text-xs uppercase tracking-wider rounded transition-all duration-300 shadow-sm backdrop-blur-md">
|
||||
Templates ansehen
|
||||
Beispiele ansehen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -49,10 +62,6 @@
|
||||
<!-- Textur-Noise -->
|
||||
<div class="absolute inset-0 opacity-[0.02] dark:opacity-[0.01] pointer-events-none bg-[radial-gradient(#000_1px,transparent_1px)] dark:bg-[radial-gradient(#fff_1px,transparent_1px)] [background-size:8px_8px] transition-opacity duration-300"></div>
|
||||
|
||||
<!-- Helle innere Sphären -->
|
||||
<div class="absolute -top-12 -right-12 w-32 h-32 rounded-full bg-sky-200/35 dark:bg-sky-400/10 blur-xl group-hover:scale-110 transition-transform duration-700"></div>
|
||||
<div class="absolute -bottom-16 -left-16 w-40 h-40 rounded-full bg-indigo-200/25 dark:bg-indigo-500/10 blur-xl group-hover:scale-110 transition-transform duration-700"></div>
|
||||
|
||||
<!-- Taktile Details -->
|
||||
<span class="absolute top-3 left-3 w-2 h-2 border-t border-l border-slate-300 dark:border-white/30"></span>
|
||||
<span class="absolute top-3 right-3 w-2 h-2 border-t border-r border-slate-300 dark:border-white/30"></span>
|
||||
@@ -60,8 +69,8 @@
|
||||
<span class="absolute bottom-3 right-3 w-2 h-2 border-b border-r border-slate-300 dark:border-white/30"></span>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-center relative z-10">
|
||||
<span class="text-[9px] font-mono text-slate-400 dark:text-zinc-500 tracking-widest">SYSTEM STATUS: ACTIVE</span>
|
||||
<div class="relative z-10 flex justify-between items-center mb-3">
|
||||
<span class="text-[9px] font-mono text-slate-400 dark:text-zinc-500 tracking-widest uppercase" id="hero-slider-counter">TEMPLATE 01 / 10</span>
|
||||
<div class="flex gap-1">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-slate-300 dark:bg-red-500/60"></span>
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-slate-300 dark:bg-yellow-500/60"></span>
|
||||
@@ -69,23 +78,83 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grafik -->
|
||||
<div class="my-auto relative z-10 flex flex-col items-center">
|
||||
<div class="w-24 h-24 rounded-full border border-sky-200 dark:border-white/10 flex items-center justify-center bg-white/60 dark:bg-white/[0.02] relative group-hover:border-sky-400/40 transition-colors duration-500 shadow-inner">
|
||||
<svg class="w-8 h-8 text-sky-600 dark:text-sky-400 stroke-[1.5]" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.813 15.904L9 21m0 0l-.813-5.096L3 15.187m6 5.813a2.25 2.25 0 100-4.5 2.25 2.25 0 000 4.5zM19.071 5.929a10 10 0 00-14.142 0M17.657 7.343a8 8 0 00-11.314 0M16.243 8.757a6 6 0 00-8.486 0M19.071 19.071L12 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-xs font-mono text-slate-800 dark:text-zinc-300 mt-4 tracking-wider uppercase font-bold">Lighthouse Perfekt</span>
|
||||
<span class="text-[10px] text-slate-500 font-mono mt-1">Speed Index: 0.4s</span>
|
||||
<!-- Dynamic Slider Content -->
|
||||
<div class="my-auto relative z-10 w-full overflow-hidden h-48">
|
||||
{heroTemplates.map((item, index) => (
|
||||
<div id={`hero-slide-${index}`} class={`hero-slide absolute inset-0 transition-all duration-500 flex flex-col justify-between ${index === 0 ? 'opacity-100 scale-100 pointer-events-auto' : 'opacity-0 scale-95 pointer-events-none'}`}>
|
||||
<div class:list={["relative w-full h-full rounded-2xl border bg-gradient-to-br p-4 flex flex-col justify-between shadow-inner", item.colors]}>
|
||||
<!-- Top bar inside preview card -->
|
||||
<div class="flex justify-between items-center border-b border-current/5 pb-2">
|
||||
<span class="text-[9px] font-mono opacity-60 tracking-wider">TEMPLATE {item.id}</span>
|
||||
<span class="text-[9px] font-mono px-1.5 py-0.5 rounded bg-current/10 uppercase font-bold">{item.layout}</span>
|
||||
</div>
|
||||
|
||||
<!-- Mini layout representations -->
|
||||
<div class="flex-grow py-3 flex gap-2 opacity-30">
|
||||
{item.layout === "bento" && (
|
||||
<div class="w-full grid grid-cols-3 gap-1">
|
||||
<div class="col-span-2 h-5 bg-current/20 rounded"></div>
|
||||
<div class="h-5 bg-current/20 rounded"></div>
|
||||
<div class="h-7 bg-current/25 rounded"></div>
|
||||
<div class="col-span-2 h-7 bg-current/20 rounded"></div>
|
||||
</div>
|
||||
)}
|
||||
{item.layout === "split" && (
|
||||
<div class="w-full grid grid-cols-2 gap-2">
|
||||
<div class="h-14 bg-current/20 rounded"></div>
|
||||
<div class="h-14 bg-current/30 rounded flex flex-col justify-end p-1"><div class="h-1.5 w-3/4 bg-current/20 rounded"></div></div>
|
||||
</div>
|
||||
)}
|
||||
{item.layout === "boxed" && (
|
||||
<div class="w-full border border-current/20 rounded p-1 flex flex-col gap-1">
|
||||
<div class="h-2 bg-current/20 rounded"></div>
|
||||
<div class="h-9 bg-current/20 rounded"></div>
|
||||
</div>
|
||||
)}
|
||||
{item.layout === "asymmetric" && (
|
||||
<div class="w-full flex gap-2">
|
||||
<div class="w-1/3 h-14 bg-current/25 rounded"></div>
|
||||
<div class="w-2/3 h-10 bg-current/15 rounded"></div>
|
||||
</div>
|
||||
)}
|
||||
{item.layout === "single" && (
|
||||
<div class="w-full flex flex-col gap-1">
|
||||
<div class="h-3 bg-current/20 rounded"></div>
|
||||
<div class="h-3 bg-current/20 rounded"></div>
|
||||
<div class="h-6 bg-current/20 rounded"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<!-- Bottom info -->
|
||||
<div class="flex justify-between items-end pt-2 border-t border-current/5">
|
||||
<div>
|
||||
<h3 class="text-xs font-bold leading-none">{item.name}</h3>
|
||||
<span class="text-[9px] opacity-60 mt-1 block">{item.style}</span>
|
||||
</div>
|
||||
<span class="text-[9px] font-mono px-1.5 py-0.5 rounded bg-current/5 uppercase">{item.design}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="relative z-10 border-t border-slate-100 dark:border-white/5 pt-4">
|
||||
<div class="flex justify-between items-baseline">
|
||||
<span class="text-2xl font-bold text-slate-900 dark:text-white tracking-tight">100 / 100</span>
|
||||
<span class="text-[9px] font-mono text-emerald-600 dark:text-emerald-400 uppercase tracking-widest font-bold">BEST SCORE</span>
|
||||
<!-- Navigation Footer -->
|
||||
<div class="relative z-10 border-t border-slate-100 dark:border-white/5 pt-4 flex justify-between items-center">
|
||||
<!-- Left/Right Buttons -->
|
||||
<div class="flex gap-2">
|
||||
<button id="hero-slider-prev" aria-label="Vorheriges Template" class="w-8 h-8 rounded-lg bg-black/5 hover:bg-black/10 dark:bg-white/5 dark:hover:bg-white/10 flex items-center justify-center text-slate-700 dark:text-white transition-colors border border-black/5 dark:border-white/5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" class="w-2.5 h-2.5 fill-current"><path d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z"/></svg>
|
||||
</button>
|
||||
<button id="hero-slider-next" aria-label="Nächstes Template" class="w-8 h-8 rounded-lg bg-black/5 hover:bg-black/10 dark:bg-white/5 dark:hover:bg-white/10 flex items-center justify-center text-slate-700 dark:text-white transition-colors border border-black/5 dark:border-white/5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" class="w-2.5 h-2.5 fill-current"><path d="M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s-32.8-12.5-45.3 0l192 192z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<a href="/templates" class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-sky-600 hover:bg-sky-700 dark:bg-sky-500/10 dark:hover:bg-sky-500/20 text-white dark:text-sky-400 font-bold text-[10px] uppercase tracking-wider rounded-lg border border-transparent dark:border-sky-500/20 transition-colors">
|
||||
Alle Templates
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" class="w-2.5 h-2.5 fill-current" aria-hidden="true"><path d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -95,7 +164,7 @@
|
||||
</section>
|
||||
|
||||
<!-- Hauptinhalte: Elegantes Hybrid-Liquid Layout (Echtes Handwerk, kein KI-Standard-Raster) -->
|
||||
<section class="py-24 bg-slate-50 dark:bg-[#070A13] relative border-t border-slate-100 dark:border-white/5 transition-colors duration-300">
|
||||
<section class="py-24 bg-slate-50 dark:bg-[#0B0F19] relative border-t border-slate-100 dark:border-white/5 transition-colors duration-300">
|
||||
<div class="absolute inset-0 opacity-[0.02] pointer-events-none bg-[radial-gradient(#000_1px,transparent_1px)] dark:bg-[radial-gradient(#fff_1px,transparent_1px)] [background-size:16px_16px]"></div>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
@@ -104,10 +173,10 @@
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 mb-16 items-start reveal">
|
||||
<div class="lg:col-span-8">
|
||||
<h2 class="text-3xl sm:text-4xl font-extrabold text-slate-900 dark:text-white tracking-tight uppercase transition-colors duration-300">
|
||||
Ihr digitaler Mitarbeiter für Neukunden.
|
||||
Ihr neuer Helfer im Internet.
|
||||
</h2>
|
||||
<p class="text-slate-600 dark:text-zinc-400 text-sm mt-4 leading-relaxed max-w-2xl transition-colors duration-300">
|
||||
Eine gute Website ist kein Statussymbol, sondern Werkzeug. Wir programmieren Ihren Web-Auftritt so, dass er vollautomatisch Anfragen von echten Kunden aus Ihrer Region generiert.
|
||||
Eine Webseite soll Ihnen Arbeit abnehmen und neue Kunden bringen. Wir bauen Ihre Seite genau so, dass Menschen aus Ihrer Region Sie ganz einfach anrufen oder eine Nachricht schreiben können.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -115,24 +184,24 @@
|
||||
<div class="lg:col-span-4 bg-white/50 border border-white dark:bg-transparent dark:border-white/5 p-6 rounded-2xl shadow-sm space-y-4 transition-colors duration-300">
|
||||
<div class="flex items-center gap-3 text-xs text-slate-700 dark:text-zinc-300 font-medium">
|
||||
<svg class="w-4 h-4 text-emerald-500 dark:text-sky-400 shrink-0 fill-current" viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg"><path d="M438.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L160 338.7 393.4 105.4c12.5-12.5 32.8-12.5 45.3 0z"/></svg>
|
||||
<span>Keine Baukästen</span>
|
||||
<span>Echte Handarbeit (keine Baukästen)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-xs text-slate-700 dark:text-zinc-300 font-medium">
|
||||
<svg class="w-4 h-4 text-emerald-500 dark:text-sky-400 shrink-0 fill-current" viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg"><path d="M438.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L160 338.7 393.4 105.4c12.5-12.5 32.8-12.5 45.3 0z"/></svg>
|
||||
<span>100% DSGVO-sicher</span>
|
||||
<span>100% Sicher vor Abmahnungen</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-xs text-slate-700 dark:text-zinc-300 font-medium">
|
||||
<svg class="w-4 h-4 text-emerald-500 dark:text-sky-400 shrink-0 fill-current" viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg"><path d="M438.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L160 338.7 393.4 105.4c12.5-12.5 32.8-12.5 45.3 0z"/></svg>
|
||||
<span>Feste Monats-Pauschale</span>
|
||||
<span>Fester, fairer Monatspreis</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Liquid Glass Service Grid -->
|
||||
<!-- Liquid Glass Bento Grid (3-Säulen-Architektur) -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
|
||||
<!-- Card 1: Webdesign -->
|
||||
<div class="relative rounded-2xl border border-white/80 dark:border-white/[0.06] bg-white/40 dark:bg-transparent 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-[260px] shadow-sm hover:shadow-md reveal">
|
||||
<!-- Card 1: WaaS Premium Webdesign (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-[260px] 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>
|
||||
|
||||
@@ -140,16 +209,19 @@
|
||||
<div class="w-10 h-10 rounded bg-sky-50 border border-sky-100 dark:bg-sky-400/5 dark:border-sky-400/10 flex items-center justify-center text-sky-600 dark:text-sky-400 group-hover:border-sky-300 dark:group-hover:border-sky-400/30 transition-all duration-300 mb-6">
|
||||
<svg class="w-5 h-5 fill-current" viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M48 24C48 10.7 37.3 0 24 0S0 10.7 0 24V488c0 13.3 10.7 24 24 24s24-10.7 24-24V24zM128 64c-17.7 0-32 14.3-32 32v48c0 17.7 14.3 32 32 32H384c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32H128zm0 160c-17.7 0-32 14.3-32 32v48c0 17.7 14.3 32 32 32H384c17.7 0 32-14.3 32-32V256c0-17.7-14.3-32-32-32H128zm0 160c-17.7 0-32 14.3-32 32v48c0 17.7 14.3 32 32 32H384c17.7 0 32-14.3 32-32V416c0-17.7-14.3-32-32-32H128z"/></svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-bold text-slate-950 dark:text-white mb-2 uppercase tracking-tight transition-colors duration-300">Sorgenfrei-Hosting</h3>
|
||||
<p class="text-slate-600 dark:text-zinc-400 text-xs leading-relaxed transition-colors duration-300">
|
||||
Deutsche Server, tägliche Backups und SSL-Verschlüsselung. Ihr Internetauftritt ist rundum abgesichert im monatlichen Abo.
|
||||
<h3 class="text-xl font-bold text-slate-950 dark:text-white mb-2 uppercase tracking-tight transition-colors duration-300">Rundum-Sorglos-Webseite</h3>
|
||||
<p class="text-slate-600 dark:text-zinc-400 text-sm leading-relaxed transition-colors duration-300">
|
||||
Wir bauen Ihre neue, verkaufsstarke Webseite fix und fertig auf. Keine teuren Einmalkosten für die Erstellung. Für eine faire monatliche Pauschale übernehmen wir das Design, die Programmierung, den Server, Sicherheits-Updates und tägliche Backups.
|
||||
</p>
|
||||
</div>
|
||||
<span class="text-[9px] font-mono text-slate-400 dark:text-zinc-600 mt-6 tracking-wider">100% Regelkonform</span>
|
||||
<div class="flex justify-between items-center mt-6 pt-4 border-t border-slate-100 dark:border-white/5">
|
||||
<span class="text-[10px] font-mono text-slate-400 dark:text-zinc-500 uppercase tracking-wider">Alles inklusive ab 49€ / Monat</span>
|
||||
<span class="text-xs font-bold text-sky-600 dark:text-sky-400">Keine versteckten Kosten</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card 2: SEO -->
|
||||
<div class="relative rounded-2xl border border-white/80 dark:border-white/[0.06] bg-white/40 dark:bg-transparent 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-[260px] shadow-sm hover:shadow-md reveal">
|
||||
<!-- Card 2: Lokale Sichtbarkeit (Schmal: col-span-1) -->
|
||||
<div class="relative 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-[260px] 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>
|
||||
|
||||
@@ -157,51 +229,42 @@
|
||||
<div class="w-10 h-10 rounded bg-sky-50 border border-sky-100 dark:bg-sky-400/5 dark:border-sky-400/10 flex items-center justify-center text-sky-600 dark:text-sky-400 group-hover:border-sky-300 dark:group-hover:border-sky-400/30 transition-all duration-300 mb-6">
|
||||
<svg class="w-5 h-5 fill-current" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"/></svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-bold text-slate-950 dark:text-white mb-2 uppercase tracking-tight transition-colors duration-300">Lokale Sichtbarkeit</h3>
|
||||
<h3 class="text-lg font-bold text-slate-950 dark:text-white mb-2 uppercase tracking-tight transition-colors duration-300">Bei Google glänzen</h3>
|
||||
<p class="text-slate-600 dark:text-zinc-400 text-xs leading-relaxed transition-colors duration-300">
|
||||
Google-Optimierung für Ihre Stadt. Damit Kunden Sie finden und nicht die Konkurrenz.
|
||||
Wir sorgen dafür, dass Kunden aus Ihrer Stadt Sie sofort finden, wenn sie bei Google nach Ihren Angeboten suchen.
|
||||
</p>
|
||||
</div>
|
||||
<span class="text-[9px] font-mono text-slate-400 dark:text-zinc-600 mt-6 tracking-wider">Google-Dominanz</span>
|
||||
<span class="text-[9px] font-mono text-slate-400 dark:text-zinc-600 mt-6 tracking-wider">Kunden aus der Region</span>
|
||||
</div>
|
||||
|
||||
<!-- Card 3: Hosting -->
|
||||
<div class="relative rounded-2xl border border-white/80 dark:border-white/[0.06] bg-white/40 dark:bg-transparent 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-[260px] shadow-sm hover:shadow-md reveal">
|
||||
<!-- Card 3: DSGVO-Sicherheit (Schmal: col-span-1) -->
|
||||
<div class="relative 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-[260px] 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>
|
||||
<div class="w-10 h-10 rounded bg-sky-50 border border-sky-100 dark:bg-sky-400/5 dark:border-sky-400/10 flex items-center justify-center text-sky-600 dark:text-sky-400 group-hover:border-sky-300 dark:group-hover:border-sky-400/30 transition-all duration-300 mb-6">
|
||||
<svg class="w-5 h-5 fill-current" viewBox="0 0 640 512" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M64 96c0-35.3 28.7-64 64-64H512c35.3 0 64 28.7 64 64V352H512V96H128V352H64V96zM0 403.2C0 392.6 8.6 384 19.2 384H620.8c10.6 0 19.2 8.6 19.2 19.2c0 42.4-34.4 76.8-76.8 76.8H76.8C34.4 480 0 445.6 0 403.2zM288 432c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16s-7.2-16-16-16H304c-8.8 0-16 7.2-16 16z"/></svg>
|
||||
<svg class="w-5 h-5 fill-current" viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M438.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L160 338.7 393.4 105.4c12.5-12.5 32.8-12.5 45.3 0z"/></svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-bold text-slate-950 dark:text-white mb-2 uppercase tracking-tight transition-colors duration-300">Webdesign Inklusive</h3>
|
||||
<h3 class="text-lg font-bold text-slate-950 dark:text-white mb-2 uppercase tracking-tight transition-colors duration-300">100% Rechtssicher</h3>
|
||||
<p class="text-slate-600 dark:text-zinc-400 text-xs leading-relaxed transition-colors duration-300">
|
||||
Noch keine eigene Website? Wir erstellen Ihren neuen, optimierten Internetauftritt direkt passend zum Hosting dazu.
|
||||
Wir bauen Ihre Seite so, dass alles rechtlich wasserdicht ist. Keine Spionage, keine fremden Cookies, kein Ärger.
|
||||
</p>
|
||||
</div>
|
||||
<span class="text-[9px] font-mono text-slate-400 dark:text-zinc-600 mt-6 tracking-wider">Codeschreinerei</span>
|
||||
<span class="text-[9px] font-mono text-slate-400 dark:text-zinc-600 mt-6 tracking-wider">Abmahnsicher</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Showcase-Link Callout -->
|
||||
<div class="mt-16 text-center reveal">
|
||||
<div class="inline-flex flex-col sm:flex-row items-center gap-4 p-4 rounded-2xl border border-white/80 dark:border-white/5 bg-white/30 dark:bg-slate-950/20 backdrop-blur-md shadow-sm">
|
||||
<span class="text-xs text-slate-600 dark:text-zinc-400">
|
||||
Sie suchen Inspiration? Entdecken Sie unsere 20 maßgeschneiderten Design-Vorlagen.
|
||||
</span>
|
||||
<a href="/templates" class="inline-flex items-center gap-2 text-xs font-bold text-sky-600 dark:text-sky-400 hover:text-sky-700 dark:hover:text-white transition-colors duration-300">
|
||||
Design-Vorlagen ansehen
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" class="w-3 h-3 fill-current" aria-hidden="true"><path d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FAQ Sektion im Hybrid Liquid Glass Look -->
|
||||
<section class="py-24 bg-slate-50 dark:bg-[#070A13] relative border-t border-slate-100 dark:border-white/5 transition-colors duration-300">
|
||||
<section class="py-24 bg-slate-50 dark:bg-[#0B0F19] relative border-t border-slate-100 dark:border-white/5 transition-colors duration-300">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-3xl font-extrabold text-slate-900 dark:text-white tracking-tight uppercase">
|
||||
@@ -331,4 +394,36 @@
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Hero Template Slider
|
||||
const slides = document.querySelectorAll('.hero-slide');
|
||||
const prevBtn = document.getElementById('hero-slider-prev');
|
||||
const nextBtn = document.getElementById('hero-slider-next');
|
||||
const counter = document.getElementById('hero-slider-counter');
|
||||
let currentIdx = 0;
|
||||
const totalSlides = slides.length;
|
||||
|
||||
function updateSlider(newIdx: number) {
|
||||
// Hide current slide
|
||||
slides[currentIdx].classList.remove('opacity-100', 'scale-100', 'pointer-events-auto');
|
||||
slides[currentIdx].classList.add('opacity-0', 'scale-95', 'pointer-events-none');
|
||||
|
||||
// Update index
|
||||
currentIdx = (newIdx + totalSlides) % totalSlides;
|
||||
|
||||
// Show new slide
|
||||
slides[currentIdx].classList.remove('opacity-0', 'scale-95', 'pointer-events-none');
|
||||
slides[currentIdx].classList.add('opacity-100', 'scale-100', 'pointer-events-auto');
|
||||
|
||||
// Update counter text
|
||||
if (counter) {
|
||||
const templateNum = String(currentIdx + 1).padStart(2, '0');
|
||||
counter.textContent = `TEMPLATE ${templateNum} / ${totalSlides}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (prevBtn && nextBtn) {
|
||||
prevBtn.addEventListener('click', () => updateSlider(currentIdx - 1));
|
||||
nextBtn.addEventListener('click', () => updateSlider(currentIdx + 1));
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,19 +1,31 @@
|
||||
---
|
||||
// src/components/Navbar.astro
|
||||
import { FaBars, FaTimes } from 'react-icons/fa';
|
||||
import { loadWaasConfigs } from '../utils/configLoader';
|
||||
|
||||
const { siteConfig } = loadWaasConfigs(Astro.request.headers.get('user-agent') ?? '');
|
||||
const header = siteConfig.header || {
|
||||
brandName: 'N&D',
|
||||
brandHighlight: 'i-t SOLUTIONS',
|
||||
ctaText: 'Erstgespräch buchen',
|
||||
ctaLink: '#contact',
|
||||
showThemeToggle: true,
|
||||
glassEffect: true
|
||||
};
|
||||
---
|
||||
|
||||
<header id="main-header" class="fixed top-0 left-0 w-full z-50 transition-all duration-300 py-3 bg-white/30 dark:bg-brand-darkBg/30 backdrop-blur-xl border-b border-slate-200 dark:border-brand-darkBorder">
|
||||
<header id="main-header" class={`fixed top-0 left-0 w-full z-50 transition-all duration-300 py-3 ${header.glassEffect !== false ? 'bg-white/30 dark:bg-brand-darkBg/30 backdrop-blur-xl border-b border-slate-200 dark:border-brand-darkBorder' : 'bg-slate-900 border-b border-white/10'}`}>
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between h-14">
|
||||
|
||||
<!-- Logo -->
|
||||
<a href="#" class="flex items-center gap-2 group">
|
||||
<span class="text-lg sm:text-xl font-bold tracking-wider text-slate-900 dark:text-white select-none transition-colors duration-300">
|
||||
N&D <span class="text-brand-arcticBlue">i-t SOLUTIONS</span>
|
||||
{header.brandName || 'N&D'} <span class="text-accent">{header.brandHighlight || 'i-t SOLUTIONS'}</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
|
||||
<!-- Desktop Nav Links -->
|
||||
<nav class="hidden md:flex items-center gap-8">
|
||||
<a href="#services" class="text-xs tracking-wider uppercase font-medium text-slate-600 dark:text-zinc-400 hover:text-brand-arcticBlue dark:hover:text-brand-arcticBlue transition-colors">Leistungen</a>
|
||||
@@ -23,22 +35,26 @@ import { FaBars, FaTimes } from 'react-icons/fa';
|
||||
|
||||
<!-- Right controls -->
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- Theme Toggle Button (44x44px target) -->
|
||||
<button id="theme-toggle" class="w-11 h-11 flex items-center justify-center rounded-lg border border-slate-200 dark:border-brand-darkBorder bg-white/50 dark:bg-zinc-900/50 text-slate-700 dark:text-zinc-400 hover:text-brand-arcticBlue dark:hover:text-brand-arcticBlue transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-sky-500" aria-label="Farbschema wechseln">
|
||||
<!-- Moon Icon -->
|
||||
<svg id="theme-toggle-dark-icon" class="hidden w-4 h-4 fill-current" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
|
||||
</svg>
|
||||
<!-- Sun Icon -->
|
||||
<svg id="theme-toggle-light-icon" class="hidden w-4 h-4 fill-current" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.46 5.05l-.707-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" fill-rule="evenodd" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</button>
|
||||
{header.showThemeToggle !== false && (
|
||||
<!-- Theme Toggle Button (44x44px target) -->
|
||||
<button id="theme-toggle" class="w-11 h-11 flex items-center justify-center rounded-lg border border-slate-200 dark:border-brand-darkBorder bg-white/50 dark:bg-zinc-900/50 text-slate-700 dark:text-zinc-400 hover:text-brand-arcticBlue dark:hover:text-brand-arcticBlue transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-sky-500" aria-label="Farbschema wechseln">
|
||||
<!-- Moon Icon -->
|
||||
<svg id="theme-toggle-dark-icon" class="hidden w-4 h-4 fill-current" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
|
||||
</svg>
|
||||
<!-- Sun Icon -->
|
||||
<svg id="theme-toggle-light-icon" class="hidden w-4 h-4 fill-current" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.46 5.05l-.707-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" fill-rule="evenodd" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<!-- CTA button -->
|
||||
<a href="#contact" class="hidden sm:inline-flex items-center justify-center px-4 py-2 text-xs font-semibold tracking-wider text-slate-700 dark:text-white border border-slate-200 dark:border-brand-darkBorder rounded bg-white/50 dark:bg-zinc-900/50 hover:border-brand-arcticBlue hover:text-brand-arcticBlue transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-sky-500" aria-label="Erstgespräch buchen">
|
||||
Erstgespräch buchen
|
||||
</a>
|
||||
{header.ctaText && (
|
||||
<a href={header.ctaLink || "#contact"} class="hidden sm:inline-flex items-center justify-center px-4 py-2 text-xs font-semibold tracking-wider text-slate-700 dark:text-white border border-slate-200 dark:border-brand-darkBorder rounded bg-white/50 dark:bg-zinc-900/50 hover:border-brand-arcticBlue hover:text-brand-arcticBlue transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-sky-500" aria-label={header.ctaText}>
|
||||
{header.ctaText}
|
||||
</a>
|
||||
)}
|
||||
|
||||
<!-- Mobile menu button (44x44px target) -->
|
||||
<button id="mobile-menu-btn" class="md:hidden w-11 h-11 flex items-center justify-center rounded bg-white dark:bg-zinc-900 text-slate-700 dark:text-white border border-slate-200 dark:border-brand-darkBorder hover:bg-slate-50 dark:hover:bg-zinc-800 transition-all focus:outline-none focus:ring-2 focus:ring-sky-500" aria-label="Menü öffnen">
|
||||
@@ -121,13 +137,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();
|
||||
});
|
||||
|
||||
873
src/components/OnboardingWizard.astro
Normal file
873
src/components/OnboardingWizard.astro
Normal file
@@ -0,0 +1,873 @@
|
||||
---
|
||||
// src/components/OnboardingWizard.astro
|
||||
---
|
||||
|
||||
<section id="onboarding-wizard" class="py-24 bg-slate-50 dark:bg-[#0B0F19] relative border-t border-slate-200 dark:border-white/5 transition-colors duration-300">
|
||||
<!-- Soft Background Glow -->
|
||||
<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]"></div>
|
||||
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[80%] h-[60%] bg-sky-500/5 dark:bg-sky-500/[0.02] blur-[120px] rounded-full pointer-events-none"></div>
|
||||
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div class="text-center mb-12">
|
||||
<span class="text-xs uppercase tracking-widest text-sky-600 dark:text-sky-400 font-bold">Webseiten-Planer</span>
|
||||
<h2 class="text-3xl sm:text-4xl font-extrabold text-slate-900 dark:text-white mt-2 uppercase tracking-tight">Ihr Wunsch-Konzept</h2>
|
||||
<p class="text-slate-600 dark:text-zinc-400 text-xs sm:text-sm mt-3">
|
||||
Planen Sie Ihre neue Webseite in unter 2 Minuten. Kostenlos und unverbindlich.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Wizard Box (Liquid Glass Style) -->
|
||||
<div class="relative rounded-3xl border border-white/80 dark:border-white/[0.06] bg-white/40 dark:bg-slate-950/40 backdrop-blur-2xl p-6 sm:p-10 shadow-xl overflow-hidden min-h-[600px] flex flex-col justify-between">
|
||||
<!-- Tactile details -->
|
||||
<span class="absolute top-3 left-3 w-2 h-2 border-t border-l border-slate-300 dark:border-white/20"></span>
|
||||
<span class="absolute top-3 right-3 w-2 h-2 border-t border-r border-slate-300 dark:border-white/20"></span>
|
||||
<span class="absolute bottom-3 left-3 w-2 h-2 border-b border-l border-slate-300 dark:border-white/20"></span>
|
||||
<span class="absolute bottom-3 right-3 w-2 h-2 border-b border-r border-slate-300 dark:border-white/20"></span>
|
||||
|
||||
<!-- Step Indicator Bar -->
|
||||
<div class="flex justify-between items-center mb-8 pb-4 border-b border-slate-200/50 dark:border-white/5">
|
||||
<span class="text-[10px] font-mono text-slate-400 dark:text-zinc-500 uppercase tracking-widest" id="step-counter">SCHRITT 1 / 5</span>
|
||||
<div class="flex gap-2" id="step-dots">
|
||||
<span class="w-2 h-2 rounded-full bg-sky-500 transition-colors" data-step="1"></span>
|
||||
<span class="w-2 h-2 rounded-full bg-slate-300 dark:bg-zinc-700 transition-colors" data-step="2"></span>
|
||||
<span class="w-2 h-2 rounded-full bg-slate-300 dark:bg-zinc-700 transition-colors" data-step="3"></span>
|
||||
<span class="w-2 h-2 rounded-full bg-slate-300 dark:bg-zinc-700 transition-colors" data-step="4"></span>
|
||||
<span class="w-2 h-2 rounded-full bg-slate-300 dark:bg-zinc-700 transition-colors" data-step="5"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Columns: Form Left, Live Preview Right -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 items-stretch flex-grow">
|
||||
|
||||
<!-- Left Column: Form Inputs (7 cols) -->
|
||||
<form id="wizard-form" class="lg:col-span-7 flex flex-col justify-between min-h-[400px]" novalidate>
|
||||
|
||||
<!-- STEP 1: Branchen-Auswahl -->
|
||||
<div class="wizard-step space-y-4" data-step="1">
|
||||
<div>
|
||||
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-1 uppercase tracking-tight">Schritt 1: Branche wählen</h3>
|
||||
<p class="text-slate-500 dark:text-zinc-400 text-xs">Suchen Sie Ihre Branche oder tragen Sie Ihre eigene ein.</p>
|
||||
</div>
|
||||
|
||||
<!-- Search Field -->
|
||||
<div class="relative">
|
||||
<input type="text" id="industry-search" placeholder="Branche suchen (z.B. Schreinerei, Arzt...)" class="w-full bg-white dark:bg-zinc-950 border border-slate-200 dark:border-brand-darkBorder rounded-xl p-3 text-xs text-slate-800 dark:text-white focus:outline-none focus:ring-2 focus:ring-sky-500 transition-all" />
|
||||
</div>
|
||||
|
||||
<!-- Industries List (Scrollable) -->
|
||||
<div class="max-h-48 overflow-y-auto border border-slate-200/60 dark:border-white/5 rounded-xl p-3 grid grid-cols-1 sm:grid-cols-2 gap-2 bg-slate-100/30 dark:bg-white/5" id="industry-list">
|
||||
<label class="cursor-pointer flex items-center gap-2 p-2 bg-white dark:bg-zinc-900 border border-transparent rounded-lg hover:border-sky-500/30 transition-all text-xs text-slate-800 dark:text-zinc-300">
|
||||
<input type="radio" name="industry" value="Schreinerei & Innenausbau" checked class="text-sky-600 focus:ring-sky-500" />
|
||||
<span>Schreinerei & Innenausbau</span>
|
||||
</label>
|
||||
<label class="cursor-pointer flex items-center gap-2 p-2 bg-white dark:bg-zinc-900 border border-transparent rounded-lg hover:border-sky-500/30 transition-all text-xs text-slate-800 dark:text-zinc-300">
|
||||
<input type="radio" name="industry" value="Dachdeckerei" class="text-sky-600 focus:ring-sky-500" />
|
||||
<span>Dachdeckerei</span>
|
||||
</label>
|
||||
<label class="cursor-pointer flex items-center gap-2 p-2 bg-white dark:bg-zinc-900 border border-transparent rounded-lg hover:border-sky-500/30 transition-all text-xs text-slate-800 dark:text-zinc-300">
|
||||
<input type="radio" name="industry" value="Solartechnik & PV" class="text-sky-600 focus:ring-sky-500" />
|
||||
<span>Solartechnik & PV</span>
|
||||
</label>
|
||||
<label class="cursor-pointer flex items-center gap-2 p-2 bg-white dark:bg-zinc-900 border border-transparent rounded-lg hover:border-sky-500/30 transition-all text-xs text-slate-800 dark:text-zinc-300">
|
||||
<input type="radio" name="industry" value="Rechtsanwälte & Notare" class="text-sky-600 focus:ring-sky-500" />
|
||||
<span>Rechtsanwälte & Notare</span>
|
||||
</label>
|
||||
<label class="cursor-pointer flex items-center gap-2 p-2 bg-white dark:bg-zinc-900 border border-transparent rounded-lg hover:border-sky-500/30 transition-all text-xs text-slate-800 dark:text-zinc-300">
|
||||
<input type="radio" name="industry" value="Bio-Hofladen" class="text-sky-600 focus:ring-sky-500" />
|
||||
<span>Bio-Hofladen</span>
|
||||
</label>
|
||||
<label class="cursor-pointer flex items-center gap-2 p-2 bg-white dark:bg-zinc-900 border border-transparent rounded-lg hover:border-sky-500/30 transition-all text-xs text-slate-800 dark:text-zinc-300">
|
||||
<input type="radio" name="industry" value="Sanitär, Heizung & Klima" class="text-sky-600 focus:ring-sky-500" />
|
||||
<span>Sanitär, Heizung & Klima</span>
|
||||
</label>
|
||||
<label class="cursor-pointer flex items-center gap-2 p-2 bg-white dark:bg-zinc-900 border border-transparent rounded-lg hover:border-sky-500/30 transition-all text-xs text-slate-800 dark:text-zinc-300">
|
||||
<input type="radio" name="industry" value="KFZ-Meisterbetrieb" class="text-sky-600 focus:ring-sky-500" />
|
||||
<span>KFZ-Meisterbetrieb</span>
|
||||
</label>
|
||||
<label class="cursor-pointer flex items-center gap-2 p-2 bg-white dark:bg-zinc-900 border border-transparent rounded-lg hover:border-sky-500/30 transition-all text-xs text-slate-800 dark:text-zinc-300">
|
||||
<input type="radio" name="industry" value="Steuerberater" class="text-sky-600 focus:ring-sky-500" />
|
||||
<span>Steuerberater</span>
|
||||
</label>
|
||||
<label class="cursor-pointer flex items-center gap-2 p-2 bg-white dark:bg-zinc-900 border border-transparent rounded-lg hover:border-sky-500/30 transition-all text-xs text-slate-800 dark:text-zinc-300">
|
||||
<input type="radio" name="industry" value="Garten- & Landschaftsbau" class="text-sky-600 focus:ring-sky-500" />
|
||||
<span>Garten- & Landschaftsbau</span>
|
||||
</label>
|
||||
<label class="cursor-pointer flex items-center gap-2 p-2 bg-white dark:bg-zinc-900 border border-transparent rounded-lg hover:border-sky-500/30 transition-all text-xs text-slate-800 dark:text-zinc-300">
|
||||
<input type="radio" name="industry" value="Physiotherapie Praxis" class="text-sky-600 focus:ring-sky-500" />
|
||||
<span>Physiotherapie Praxis</span>
|
||||
</label>
|
||||
<label class="cursor-pointer flex items-center gap-2 p-2 bg-white dark:bg-zinc-900 border border-transparent rounded-lg hover:border-sky-500/30 transition-all text-xs text-slate-800 dark:text-zinc-300">
|
||||
<input type="radio" name="industry" value="Maler- & Lackiererbetrieb" class="text-sky-600 focus:ring-sky-500" />
|
||||
<span>Maler- & Lackiererbetrieb</span>
|
||||
</label>
|
||||
<label class="cursor-pointer flex items-center gap-2 p-2 bg-white dark:bg-zinc-900 border border-transparent rounded-lg hover:border-sky-500/30 transition-all text-xs text-slate-800 dark:text-zinc-300">
|
||||
<input type="radio" name="industry" value="Zahnarztpraxis" class="text-sky-600 focus:ring-sky-500" />
|
||||
<span>Zahnarztpraxis</span>
|
||||
</label>
|
||||
<label class="cursor-pointer flex items-center gap-2 p-2 bg-white dark:bg-zinc-900 border border-transparent rounded-lg hover:border-sky-500/30 transition-all text-xs text-slate-800 dark:text-zinc-300">
|
||||
<input type="radio" name="industry" value="Architekturbüro" class="text-sky-600 focus:ring-sky-500" />
|
||||
<span>Architekturbüro</span>
|
||||
</label>
|
||||
<label class="cursor-pointer flex items-center gap-2 p-2 bg-white dark:bg-zinc-900 border border-transparent rounded-lg hover:border-sky-500/30 transition-all text-xs text-slate-800 dark:text-zinc-300">
|
||||
<input type="radio" name="industry" value="Software-Entwicklung" class="text-sky-600 focus:ring-sky-500" />
|
||||
<span>Software-Entwicklung</span>
|
||||
</label>
|
||||
<label class="cursor-pointer flex items-center gap-2 p-2 bg-white dark:bg-zinc-900 border border-transparent rounded-lg hover:border-sky-500/30 transition-all text-xs text-slate-800 dark:text-zinc-300">
|
||||
<input type="radio" name="industry" value="Bäckerei & Konditorei" class="text-sky-600 focus:ring-sky-500" />
|
||||
<span>Bäckerei & Konditorei</span>
|
||||
</label>
|
||||
<label class="cursor-pointer flex items-center gap-2 p-2 bg-white dark:bg-zinc-900 border border-transparent rounded-lg hover:border-sky-500/30 transition-all text-xs text-slate-800 dark:text-zinc-300">
|
||||
<input type="radio" name="industry" value="Friseursalon" class="text-sky-600 focus:ring-sky-500" />
|
||||
<span>Friseursalon</span>
|
||||
</label>
|
||||
<label class="cursor-pointer flex items-center gap-2 p-2 bg-white dark:bg-zinc-900 border border-transparent rounded-lg hover:border-sky-500/30 transition-all text-xs text-slate-800 dark:text-zinc-300">
|
||||
<input type="radio" name="industry" value="Kosmetikstudio" class="text-sky-600 focus:ring-sky-500" />
|
||||
<span>Kosmetikstudio</span>
|
||||
</label>
|
||||
<label class="cursor-pointer flex items-center gap-2 p-2 bg-white dark:bg-zinc-900 border border-transparent rounded-lg hover:border-sky-500/30 transition-all text-xs text-slate-800 dark:text-zinc-300">
|
||||
<input type="radio" name="industry" value="Restaurant & Gastronomie" class="text-sky-600 focus:ring-sky-500" />
|
||||
<span>Restaurant & Gastronomie</span>
|
||||
</label>
|
||||
<label class="cursor-pointer flex items-center gap-2 p-2 bg-white dark:bg-zinc-900 border border-transparent rounded-lg hover:border-sky-500/30 transition-all text-xs text-slate-800 dark:text-zinc-300">
|
||||
<input type="radio" name="industry" value="Elektro-Technik" class="text-sky-600 focus:ring-sky-500" />
|
||||
<span>Elektro-Technik</span>
|
||||
</label>
|
||||
<label class="cursor-pointer flex items-center gap-2 p-2 bg-white dark:bg-zinc-900 border border-transparent rounded-lg hover:border-sky-500/30 transition-all text-xs text-slate-800 dark:text-zinc-300">
|
||||
<input type="radio" name="industry" value="Gebäudereinigung" class="text-sky-600 focus:ring-sky-500" />
|
||||
<span>Gebäudereinigung</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Custom / Individual Field -->
|
||||
<div class="pt-2">
|
||||
<label for="industry-custom" class="block text-[10px] font-bold text-slate-500 dark:text-zinc-400 uppercase mb-1">Eigene / Andere Branche</label>
|
||||
<input type="text" id="industry-custom" placeholder="Ihre Branche eintragen..." class="w-full bg-white dark:bg-zinc-950 border border-slate-200 dark:border-brand-darkBorder rounded-xl p-2.5 text-xs text-slate-800 dark:text-white focus:outline-none focus:ring-2 focus:ring-sky-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 2: Farbauswahl -->
|
||||
<div class="wizard-step space-y-6 hidden" data-step="2">
|
||||
<div>
|
||||
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-1 uppercase tracking-tight">Schritt 2: Farben</h3>
|
||||
<p class="text-slate-500 dark:text-zinc-400 text-xs">Wählen Sie empfohlene branchenspezifische oder klassische Farbkombinationen.</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Industry Recommendation Section -->
|
||||
<div class="p-4 rounded-2xl border border-sky-500/20 bg-sky-500/5" id="industry-rec-box">
|
||||
<span class="text-[9px] font-mono text-sky-600 dark:text-sky-400 uppercase tracking-widest font-bold">Empfehlung für Ihre Branche</span>
|
||||
<div class="flex items-center justify-between mt-2">
|
||||
<span class="text-xs font-bold text-slate-800 dark:text-zinc-200" id="rec-label">Branchen-Kombination</span>
|
||||
<button type="button" class="px-4 py-2 bg-sky-600 hover:bg-sky-700 dark:bg-sky-500 dark:hover:bg-white text-white dark:text-zinc-950 font-bold text-[10px] uppercase tracking-wider rounded-lg transition-all" id="apply-rec-btn">
|
||||
Anwenden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Standard Harmonious Combos -->
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-500 dark:text-zinc-400 uppercase mb-2">Standard-Farbkombinationen</label>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
<button type="button" class="color-preset-btn p-2 rounded-xl border border-slate-200 dark:border-white/10 text-[10px] font-bold bg-slate-100 dark:bg-zinc-900 text-slate-800 dark:text-zinc-100 flex items-center gap-1.5 hover:border-sky-500" data-primary="#3b82f6" data-secondary="#ffffff">
|
||||
<span class="w-3 h-3 rounded-full bg-[#3b82f6]"></span>
|
||||
<span>Klassisch Blau</span>
|
||||
</button>
|
||||
<button type="button" class="color-preset-btn p-2 rounded-xl border border-slate-200 dark:border-white/10 text-[10px] font-bold bg-slate-100 dark:bg-zinc-900 text-slate-800 dark:text-zinc-100 flex items-center gap-1.5 hover:border-sky-500" data-primary="#10b981" data-secondary="#e2e8f0">
|
||||
<span class="w-3 h-3 rounded-full bg-[#10b981]"></span>
|
||||
<span>Smaragd & Silber</span>
|
||||
</button>
|
||||
<button type="button" class="color-preset-btn p-2 rounded-xl border border-slate-200 dark:border-white/10 text-[10px] font-bold bg-slate-100 dark:bg-zinc-900 text-slate-800 dark:text-zinc-100 flex items-center gap-1.5 hover:border-sky-500" data-primary="#6366f1" data-secondary="#c7d2fe">
|
||||
<span class="w-3 h-3 rounded-full bg-[#6366f1]"></span>
|
||||
<span>Purple & Lilac</span>
|
||||
</button>
|
||||
<button type="button" class="color-preset-btn p-2 rounded-xl border border-slate-200 dark:border-white/10 text-[10px] font-bold bg-slate-100 dark:bg-zinc-900 text-slate-800 dark:text-zinc-100 flex items-center gap-1.5 hover:border-sky-500" data-primary="#4b5563" data-secondary="#f59e0b">
|
||||
<span class="w-3 h-3 rounded-full bg-[#4b5563]"></span>
|
||||
<span>Coal & Amber</span>
|
||||
</button>
|
||||
<button type="button" class="color-preset-btn p-2 rounded-xl border border-slate-200 dark:border-white/10 text-[10px] font-bold bg-slate-100 dark:bg-zinc-900 text-slate-800 dark:text-zinc-100 flex items-center gap-1.5 hover:border-sky-500" data-primary="#f43f5e" data-secondary="#fbbf24">
|
||||
<span class="w-3 h-3 rounded-full bg-[#f43f5e]"></span>
|
||||
<span>Muted Rose</span>
|
||||
</button>
|
||||
<button type="button" class="color-preset-btn p-2 rounded-xl border border-slate-200 dark:border-white/10 text-[10px] font-bold bg-slate-100 dark:bg-zinc-900 text-slate-800 dark:text-zinc-100 flex items-center gap-1.5 hover:border-sky-500" data-primary="#06b6d4" data-secondary="#ffffff">
|
||||
<span class="w-3 h-3 rounded-full bg-[#06b6d4]"></span>
|
||||
<span>Ocean Cyan</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manual Color Pickers -->
|
||||
<div class="grid grid-cols-2 gap-4 pt-2 border-t border-slate-200/50 dark:border-white/5">
|
||||
<div class="space-y-2">
|
||||
<label for="primary-color" class="block text-[10px] font-bold text-slate-500 dark:text-zinc-400 uppercase">Primärfarbe (Fokus)</label>
|
||||
<div class="flex items-center gap-3">
|
||||
<input type="color" id="primary-color" value="#38bdf8" class="w-10 h-10 rounded cursor-pointer border border-slate-200 dark:border-white/10 bg-transparent" />
|
||||
<span class="text-xs font-mono font-bold" id="primary-color-hex">#38BDF8</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label for="secondary-color" class="block text-[10px] font-bold text-slate-500 dark:text-zinc-400 uppercase">Sekundärfarbe</label>
|
||||
<div class="flex items-center gap-3">
|
||||
<input type="color" id="secondary-color" value="#ffffff" class="w-10 h-10 rounded cursor-pointer border border-slate-200 dark:border-white/10 bg-transparent" />
|
||||
<span class="text-xs font-mono font-bold" id="secondary-color-hex">#FFFFFF</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 3: Layout & Design-Richtung -->
|
||||
<div class="wizard-step space-y-6 hidden" data-step="3">
|
||||
<div>
|
||||
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-1 uppercase tracking-tight">Schritt 3: Layout & Stil</h3>
|
||||
<p class="text-slate-500 dark:text-zinc-400 text-xs">Konfigurieren Sie Layout-Struktur und Designrichtung der Website.</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Layout type -->
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-500 dark:text-zinc-400 uppercase mb-2">Layout-Art</label>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
<label class="cursor-pointer border border-slate-200 dark:border-white/5 bg-slate-100/50 dark:bg-white/5 p-3 rounded-lg flex items-center justify-between hover:border-sky-500 transition-all text-xs">
|
||||
<input type="radio" name="layout" value="bento" checked class="sr-only" />
|
||||
<span>Bento-Grid</span>
|
||||
</label>
|
||||
<label class="cursor-pointer border border-slate-200 dark:border-white/5 bg-slate-100/50 dark:bg-white/5 p-3 rounded-lg flex items-center justify-between hover:border-sky-500 transition-all text-xs">
|
||||
<input type="radio" name="layout" value="split" class="sr-only" />
|
||||
<span>Split-Screen</span>
|
||||
</label>
|
||||
<label class="cursor-pointer border border-slate-200 dark:border-white/5 bg-slate-100/50 dark:bg-white/5 p-3 rounded-lg flex items-center justify-between hover:border-sky-500 transition-all text-xs">
|
||||
<input type="radio" name="layout" value="boxed" class="sr-only" />
|
||||
<span>Boxed Layout</span>
|
||||
</label>
|
||||
<label class="cursor-pointer border border-slate-200 dark:border-white/5 bg-slate-100/50 dark:bg-white/5 p-3 rounded-lg flex items-center justify-between hover:border-sky-500 transition-all text-xs">
|
||||
<input type="radio" name="layout" value="asymmetric" class="sr-only" />
|
||||
<span>Asymmetrisch</span>
|
||||
</label>
|
||||
<label class="cursor-pointer border border-slate-200 dark:border-white/5 bg-slate-100/50 dark:bg-white/5 p-3 rounded-lg flex items-center justify-between hover:border-sky-500 transition-all text-xs">
|
||||
<input type="radio" name="layout" value="single" class="sr-only" />
|
||||
<span>Single-Page</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Design Style -->
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-500 dark:text-zinc-400 uppercase mb-2">Design-Richtung</label>
|
||||
<div class="grid grid-cols-2 gap-2" id="design-style-group">
|
||||
<label class="cursor-pointer border border-slate-200 dark:border-white/5 bg-slate-100/50 dark:bg-white/5 p-3 rounded-lg flex items-center justify-between hover:border-sky-500 transition-all text-xs">
|
||||
<input type="radio" name="style" value="corporate-dark" checked class="sr-only" />
|
||||
<span>Corporate Dark (Edler Darkmode)</span>
|
||||
</label>
|
||||
<label class="cursor-pointer border border-slate-200 dark:border-white/5 bg-slate-100/50 dark:bg-white/5 p-3 rounded-lg flex items-center justify-between hover:border-sky-500 transition-all text-xs">
|
||||
<input type="radio" name="style" value="nordic-light" class="sr-only" />
|
||||
<span>Nordic Light (Skandinavisch Hell)</span>
|
||||
</label>
|
||||
<label class="cursor-pointer border border-slate-200 dark:border-white/5 bg-slate-100/50 dark:bg-white/5 p-3 rounded-lg flex items-center justify-between hover:border-sky-500 transition-all text-xs">
|
||||
<input type="radio" name="style" value="industrial-gold" class="sr-only" />
|
||||
<span>Industrial Gold (Mittel-Anthrazit)</span>
|
||||
</label>
|
||||
<label class="cursor-pointer border border-slate-200 dark:border-white/5 bg-slate-100/50 dark:bg-white/5 p-3 rounded-lg flex items-center justify-between hover:border-sky-500 transition-all text-xs">
|
||||
<input type="radio" name="style" value="brutalism" class="sr-only" />
|
||||
<span>Brutalismus (Kräftige Ränder)</span>
|
||||
</label>
|
||||
<label class="cursor-pointer border border-slate-200 dark:border-white/5 bg-slate-100/50 dark:bg-white/5 p-3 rounded-lg flex items-center justify-between hover:border-sky-500 transition-all text-xs">
|
||||
<input type="radio" name="style" value="neomorphism" class="sr-only" />
|
||||
<span>Neomorphismus (Weiche Tiefen)</span>
|
||||
</label>
|
||||
<label class="cursor-pointer border border-slate-200 dark:border-white/5 bg-slate-100/50 dark:bg-white/5 p-3 rounded-lg flex items-center justify-between hover:border-sky-500 transition-all text-xs">
|
||||
<input type="radio" name="style" value="saas-minimalism" class="sr-only" />
|
||||
<span>SaaS-Minimalismus (Clean)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 4: Kontaktdaten -->
|
||||
<div class="wizard-step space-y-6 hidden" data-step="4">
|
||||
<div>
|
||||
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-2 uppercase tracking-tight">Schritt 4: Kontaktdaten</h3>
|
||||
<p class="text-slate-500 dark:text-zinc-400 text-xs">Wohin dürfen wir Ihren kostenlosen Entwurf senden?</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="wizard-name" class="block text-[10px] font-bold text-slate-500 dark:text-zinc-400 uppercase mb-1">Ihr Name *</label>
|
||||
<input type="text" id="wizard-name" required 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" placeholder="Ihr Name" />
|
||||
<p class="wizard-error text-[10px] text-red-500 mt-1 hidden">Bitte tragen Sie Ihren Namen ein.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="wizard-company" class="block text-[10px] font-bold text-slate-500 dark:text-zinc-400 uppercase mb-1">Name Ihres Ladens / Betriebes</label>
|
||||
<input type="text" id="wizard-company" 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" placeholder="z.B. Dönereck" />
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<label for="wizard-email" class="block text-[10px] font-bold text-slate-500 dark:text-zinc-400 uppercase mb-1">E-Mail-Adresse *</label>
|
||||
<input type="email" id="wizard-email" required 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" placeholder="ihre.adresse@firma.de" />
|
||||
<p class="wizard-error text-[10px] text-red-500 mt-1 hidden">Bitte eine gültige E-Mail-Adresse angeben.</p>
|
||||
</div>
|
||||
|
||||
<!-- GDPR Consent -->
|
||||
<div class="sm:col-span-2 space-y-3 pt-2">
|
||||
<p class="text-[10px] text-emerald-600 dark:text-emerald-400 font-medium">
|
||||
🛡️ Keine lästigen Werbeanrufe. Kein Spam. Nur Ihr fertiger Entwurf innerhalb von 24 Stunden.
|
||||
</p>
|
||||
<label class="flex items-start gap-2 cursor-pointer">
|
||||
<input type="checkbox" id="wizard-privacy" required class="mt-0.5 w-4 h-4 rounded text-sky-600 focus:ring-sky-500" />
|
||||
<span class="text-[10px] text-slate-500 dark:text-zinc-400 leading-normal">
|
||||
Ich bin mit der <a href="/privacy" class="text-sky-600 dark:text-sky-400 hover:underline">Datenschutzerklärung</a> einverstanden. *
|
||||
</span>
|
||||
</label>
|
||||
<p class="wizard-error text-[10px] text-red-500 hidden">Zustimmung erforderlich.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 5: Zusammenfassung & Absenden -->
|
||||
<div class="wizard-step space-y-6 hidden" data-step="5">
|
||||
<div>
|
||||
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-2 uppercase tracking-tight">Schritt 5: Prüfen & Absenden</h3>
|
||||
<p class="text-slate-500 dark:text-zinc-400 text-xs">Schauen Sie noch einmal kurz drüber. Wenn alles stimmt, schicken Sie es ab.</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-slate-100/50 dark:bg-white/5 border border-slate-200 dark:border-white/5 rounded-xl p-4 space-y-3">
|
||||
<div class="flex justify-between text-xs border-b border-slate-200 dark:border-white/5 pb-2">
|
||||
<span class="text-slate-500">Branche:</span>
|
||||
<span class="font-bold text-slate-900 dark:text-white" id="summary-industry">-</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-xs border-b border-slate-200 dark:border-white/5 pb-2">
|
||||
<span class="text-slate-500">Gewählte Farben:</span>
|
||||
<span class="font-bold text-slate-900 dark:text-white" id="summary-colors">-</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-xs border-b border-slate-200 dark:border-white/5 pb-2">
|
||||
<span class="text-slate-500">Aussehen & Aufbau:</span>
|
||||
<span class="font-bold text-slate-900 dark:text-white text-right" id="summary-layout">-</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-xs pt-1">
|
||||
<span class="text-slate-500">Name & E-Mail:</span>
|
||||
<span class="font-bold text-slate-900 dark:text-white" id="summary-contact">-</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-[10px] text-slate-500 dark:text-zinc-500 leading-normal text-center">
|
||||
Sobald Sie auf 'Absenden' klicken, machen wir uns an die Arbeit und erstellen Ihren persönlichen Entwurf.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Buttons -->
|
||||
<div class="flex justify-between gap-4 pt-8 border-t border-slate-200/50 dark:border-white/5 mt-8">
|
||||
<button type="button" id="prev-step" class="px-5 py-2.5 bg-slate-200 dark:bg-slate-900 text-slate-800 dark:text-white text-xs font-bold uppercase tracking-wider rounded-lg disabled:opacity-30 disabled:pointer-events-none transition-all">
|
||||
Zurück
|
||||
</button>
|
||||
<button type="button" id="next-step" class="px-5 py-2.5 bg-sky-600 hover:bg-sky-700 dark:bg-sky-500 dark:hover:bg-white text-white dark:text-zinc-950 text-xs font-bold uppercase tracking-wider rounded-lg transition-all">
|
||||
Weiter
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<!-- Right Column: Live Bento Preview (5 cols) -->
|
||||
<div class="lg:col-span-5 flex items-center justify-center">
|
||||
<div id="wizard-live-preview" class="w-full h-full min-h-[300px] rounded-3xl border p-6 flex flex-col justify-between overflow-hidden relative transition-all duration-500 shadow-xl bg-[#0B0F19]">
|
||||
<!-- Inner Aurora Glow Element -->
|
||||
<div id="preview-glow" class="absolute w-24 h-24 rounded-full blur-2xl pointer-events-none transition-all duration-500 top-1/4 left-1/4 bg-sky-500/20"></div>
|
||||
<div class="absolute inset-0 opacity-[0.02] pointer-events-none bg-[radial-gradient(#fff_1px,transparent_1px)] [background-size:8px_8px]"></div>
|
||||
|
||||
<!-- Top Row (Mini Header Mock) -->
|
||||
<div class="relative z-10 flex justify-between items-center pb-2 border-b border-white/10" id="preview-header">
|
||||
<div class="flex gap-1.5 items-center">
|
||||
<span class="w-2 h-2 rounded-full bg-[#38BDF8]" id="preview-logo-dot"></span>
|
||||
<span class="text-[9px] font-bold tracking-tight uppercase" id="preview-logo-text">Website Mockup</span>
|
||||
</div>
|
||||
<span class="text-[7px] font-mono opacity-50" id="preview-online-status">PREVIEW</span>
|
||||
</div>
|
||||
|
||||
<!-- Middle Content (Dynamic layouts render here) -->
|
||||
<div class="relative z-10 flex-grow py-4 flex flex-col justify-center min-h-[140px]" id="preview-content-area">
|
||||
|
||||
<!-- Bento Layout Preview Container -->
|
||||
<div id="layout-bento-mock" class="grid grid-cols-3 gap-2 w-full">
|
||||
<div class="col-span-2 p-3 rounded-lg border border-white/10 bg-white/5 flex flex-col justify-between" id="bento-box-1">
|
||||
<span class="text-[10px] font-bold leading-tight" id="preview-h1">Meisterbetrieb</span>
|
||||
<span class="text-[6px] opacity-60 mt-1" id="preview-desc">Professioneller Service vor Ort.</span>
|
||||
</div>
|
||||
<div class="p-3 rounded-lg border border-white/10 bg-white/5 flex flex-col justify-center items-center" id="bento-box-2">
|
||||
<span class="text-[8px] font-mono uppercase tracking-widest text-[#38BDF8]" id="preview-tier-badge">AKTIV</span>
|
||||
<span class="text-[5px] opacity-40 uppercase">STATUS</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Split Layout Preview Container (Hidden by default) -->
|
||||
<div id="layout-split-mock" class="grid grid-cols-2 gap-2 w-full hidden">
|
||||
<div class="p-3 rounded-lg border border-white/10 bg-white/5 flex flex-col justify-between" id="split-box-1">
|
||||
<span class="text-[10px] font-bold leading-tight" id="preview-h1-split">Regionaler Partner</span>
|
||||
<span class="text-[6px] opacity-60 mt-1" id="preview-desc-split">Echtes Handwerk, beste Qualität.</span>
|
||||
</div>
|
||||
<div class="p-3 rounded-lg border border-white/10 bg-white/5 flex items-center justify-center">
|
||||
<span class="w-6 h-6 rounded-full border border-white/20 flex items-center justify-center text-[7px]" id="split-icon">✓</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Boxed Layout Preview Container (Hidden) -->
|
||||
<div id="layout-boxed-mock" class="w-full border border-white/10 p-3 rounded-lg bg-white/5 flex flex-col gap-1 hidden" id="boxed-container">
|
||||
<div class="h-2 w-12 bg-white/10 rounded" id="boxed-accent-bar"></div>
|
||||
<span class="text-[9px] font-bold" id="preview-h1-boxed">Kompakter Titel</span>
|
||||
<p class="text-[6px] opacity-60" id="preview-desc-boxed">Zentrierter Text mit reduziertem Rahmen.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Bottom Row (Footer Mock) -->
|
||||
<div class="relative z-10 flex justify-between items-center pt-2 border-t border-white/10" id="preview-footer">
|
||||
<span class="text-[6px] opacity-40 font-mono" id="preview-bottom-left">ZERO THIRD-PARTY</span>
|
||||
<span class="text-[7px] font-bold uppercase tracking-wider px-2 py-0.5 rounded bg-[#38BDF8] text-zinc-950" id="preview-cta-badge">KONZEPT</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
/* Custom checked states for radio buttons */
|
||||
label:has(input[type="radio"]:checked) {
|
||||
@apply border-sky-500 ring-2 ring-sky-500/20 bg-sky-500/5;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
let currentStep = 1;
|
||||
const totalSteps = 5;
|
||||
|
||||
const steps = document.querySelectorAll('.wizard-step');
|
||||
const dots = document.querySelectorAll('#step-dots span');
|
||||
const counter = document.getElementById('step-counter');
|
||||
const prevBtn = document.getElementById('prev-step') as HTMLButtonElement;
|
||||
const nextBtn = document.getElementById('next-step') as HTMLButtonElement;
|
||||
const form = document.getElementById('wizard-form') as HTMLFormElement;
|
||||
|
||||
// Search and filter logic
|
||||
const industrySearch = document.getElementById('industry-search') as HTMLInputElement;
|
||||
const industryList = document.getElementById('industry-list');
|
||||
const industryLabels = industryList?.querySelectorAll('label');
|
||||
const industryCustomInput = document.getElementById('industry-custom') as HTMLInputElement;
|
||||
|
||||
// Recommendation logic elements
|
||||
const recLabel = document.getElementById('rec-label');
|
||||
const applyRecBtn = document.getElementById('apply-rec-btn');
|
||||
|
||||
// Color Inputs
|
||||
const primaryColorInput = document.getElementById('primary-color') as HTMLInputElement;
|
||||
const secondaryColorInput = document.getElementById('secondary-color') as HTMLInputElement;
|
||||
const primaryHex = document.getElementById('primary-color-hex');
|
||||
const secondaryHex = document.getElementById('secondary-color-hex');
|
||||
const colorPresetBtns = document.querySelectorAll('.color-preset-btn');
|
||||
|
||||
// Layout & Style Inputs
|
||||
const layoutInputs = form.querySelectorAll('input[name="layout"]');
|
||||
const styleInputs = form.querySelectorAll('input[name="style"]');
|
||||
|
||||
// Live Preview elements
|
||||
const previewBox = document.getElementById('wizard-live-preview');
|
||||
const previewGlow = document.getElementById('preview-glow');
|
||||
const logoDot = document.getElementById('preview-logo-dot');
|
||||
const logoText = document.getElementById('preview-logo-text');
|
||||
const badge = document.getElementById('preview-cta-badge');
|
||||
const tierBadge = document.getElementById('preview-tier-badge');
|
||||
const header = document.getElementById('preview-header');
|
||||
const footer = document.getElementById('preview-footer');
|
||||
const onlineStatus = document.getElementById('preview-online-status');
|
||||
const b1 = document.getElementById('bento-box-1');
|
||||
const b2 = document.getElementById('bento-box-2');
|
||||
const s1 = document.getElementById('split-box-1');
|
||||
const s2 = document.getElementById('split-icon');
|
||||
const bx = document.getElementById('boxed-container');
|
||||
const bxAccent = document.getElementById('boxed-accent-bar');
|
||||
|
||||
// Preview Text Nodes for Industry updates
|
||||
const previewH1 = document.getElementById('preview-h1');
|
||||
const previewDesc = document.getElementById('preview-desc');
|
||||
const previewH1Split = document.getElementById('preview-h1-split');
|
||||
const previewDescSplit = document.getElementById('preview-desc-split');
|
||||
const previewH1Boxed = document.getElementById('preview-h1-boxed');
|
||||
const previewDescBoxed = document.getElementById('preview-desc-boxed');
|
||||
|
||||
// Preview Layout mock blocks
|
||||
const bentoMock = document.getElementById('layout-bento-mock');
|
||||
const splitMock = document.getElementById('layout-split-mock');
|
||||
const boxedMock = document.getElementById('layout-boxed-mock');
|
||||
|
||||
// Step 4 inputs
|
||||
const nameInput = document.getElementById('wizard-name') as HTMLInputElement;
|
||||
const emailInput = document.getElementById('wizard-email') as HTMLInputElement;
|
||||
const privacyInput = document.getElementById('wizard-privacy') as HTMLInputElement;
|
||||
|
||||
// Summary fields
|
||||
const summaryIndustry = document.getElementById('summary-industry');
|
||||
const summaryColors = document.getElementById('summary-colors');
|
||||
const summaryLayout = document.getElementById('summary-layout');
|
||||
const summaryContact = document.getElementById('summary-contact');
|
||||
|
||||
// Color combination mapping per industry
|
||||
const industryColorConfigs: Record<string, { primary: string, secondary: string, name: string, label: string }> = {
|
||||
'Schreinerei & Innenausbau': { primary: '#9C7A4E', secondary: '#DE6B32', name: 'Möbel-Schreinerei', label: 'Eichenholz & Kupfer' },
|
||||
'Dachdeckerei': { primary: '#f59e0b', secondary: '#475569', name: 'Dachdecker Meister', label: 'Ziegel-Amber & Schiefer' },
|
||||
'Solartechnik & PV': { primary: '#ff5500', secondary: '#3f3f46', name: 'Solar PV Tech', label: 'Sonnen-Orange & Carbon' },
|
||||
'Rechtsanwälte & Notare': { primary: '#047857', secondary: '#f1f5f9', name: 'Kanzlei Expertise', label: 'Tief-Smaragd & Silber' },
|
||||
'Bio-Hofladen': { primary: '#15803d', secondary: '#c2410c', name: 'Bio-Hofladen Natur', label: 'Bio-Grün & Terracotta' },
|
||||
'Sanitär, Heizung & Klima': { primary: '#1d4ed8', secondary: '#06b6d4', name: 'SHK Meisterbetrieb', label: 'Heizungs-Blau & Aqua-Cyan' },
|
||||
'KFZ-Meisterbetrieb': { primary: '#dc2626', secondary: '#94a3b8', name: 'KFZ Schnellservice', label: 'Racing-Rot & Asphalt' },
|
||||
'Steuerberater': { primary: '#d97706', secondary: '#1e3a8a', name: 'Steuerberater Premium', label: 'Kanzlei-Gold & Royal-Blau' },
|
||||
'Garten- & Landschaftsbau': { primary: '#16a34a', secondary: '#e7e5e4', name: 'Gartengestaltung', label: 'Blatt-Grün & Sandstein' },
|
||||
'Physiotherapie Praxis': { primary: '#0d9488', secondary: '#ccfbf1', name: 'Physio Praxis', label: 'Gesundheits-Teal & Mint' },
|
||||
'Maler- & Lackiererbetrieb': { primary: '#4f46e5', secondary: '#db2777', name: 'Malerbetrieb', label: 'Kreativ-Indigo & Magenta' },
|
||||
'Zahnarztpraxis': { primary: '#38bdf8', secondary: '#f8fafc', name: 'Zahnarztpraxis', label: 'Eis-Blau & Praxis-Weiß' },
|
||||
'Architekturbüro': { primary: '#71717a', secondary: '#09090b', name: 'Architektur Struktur', label: 'Konzept-Grau & Matt-Schwarz' },
|
||||
'Software-Entwicklung': { primary: '#8b5cf6', secondary: '#00ffff', name: 'Byte Code', label: 'Electric-Purple & Cyber-Cyan' },
|
||||
'Bäckerei & Konditorei': { primary: '#d97706', secondary: '#451a03', name: 'Bäckerei Ofenfrisch', label: 'Weizen-Gold & Schokolade' },
|
||||
'Friseursalon': { primary: '#db2777', secondary: '#f3f4f6', name: 'Haardesign Salon', label: 'Rose Gold & Seiden-Weiß' },
|
||||
'Kosmetikstudio': { primary: '#a855f7', secondary: '#fafaf9', name: 'Kosmetik Oase', label: 'Lavendel & Perlglanz' },
|
||||
'Restaurant & Gastronomie': { primary: '#991b1b', secondary: '#d97706', name: 'Gourmet Restaurant', label: 'Wein-Rot & Ocker-Gold' },
|
||||
'Elektro-Technik': { primary: '#eab308', secondary: '#1f2937', name: 'Elektrotechnik', label: 'Strom-Gelb & Anthrazit' },
|
||||
'Gebäudereinigung': { primary: '#06b6d4', secondary: '#ffffff', name: 'Gebäudereinigung', label: 'Glanz-Cyan & Reinweiß' }
|
||||
};
|
||||
|
||||
// Filter list by search query
|
||||
if (industrySearch) {
|
||||
industrySearch.addEventListener('input', () => {
|
||||
const query = industrySearch.value.toLowerCase().trim();
|
||||
industryLabels?.forEach(label => {
|
||||
const text = label.textContent?.toLowerCase() || '';
|
||||
if (text.includes(query)) {
|
||||
label.style.display = 'flex';
|
||||
} else {
|
||||
label.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Color preset button actions
|
||||
colorPresetBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const primary = btn.getAttribute('data-primary') || '#38bdf8';
|
||||
const secondary = btn.getAttribute('data-secondary') || '#ffffff';
|
||||
applyColorCombo(primary, secondary);
|
||||
});
|
||||
});
|
||||
|
||||
function applyColorCombo(primary: string, secondary: string) {
|
||||
if (primaryColorInput && secondaryColorInput) {
|
||||
primaryColorInput.value = primary;
|
||||
secondaryColorInput.value = secondary;
|
||||
primaryColorInput.dispatchEvent(new Event('input'));
|
||||
secondaryColorInput.dispatchEvent(new Event('input'));
|
||||
}
|
||||
}
|
||||
|
||||
if (primaryColorInput && secondaryColorInput) {
|
||||
primaryColorInput.addEventListener('input', () => {
|
||||
if (primaryHex) primaryHex.textContent = primaryColorInput.value.toUpperCase();
|
||||
updatePreviewStyles();
|
||||
});
|
||||
secondaryColorInput.addEventListener('input', () => {
|
||||
if (secondaryHex) secondaryHex.textContent = secondaryColorInput.value.toUpperCase();
|
||||
updatePreviewStyles();
|
||||
});
|
||||
}
|
||||
|
||||
// Reacting to layout changes
|
||||
layoutInputs.forEach(input => input.addEventListener('change', updatePreviewLayout));
|
||||
styleInputs.forEach(input => input.addEventListener('change', updatePreviewStyles));
|
||||
|
||||
// Dynamic Preview Layout
|
||||
function updatePreviewLayout() {
|
||||
const activeLayout = (form.querySelector('input[name="layout"]:checked') as HTMLInputElement)?.value || 'bento';
|
||||
|
||||
bentoMock?.classList.add('hidden');
|
||||
splitMock?.classList.add('hidden');
|
||||
boxedMock?.classList.add('hidden');
|
||||
|
||||
if (activeLayout === 'bento') bentoMock?.classList.remove('hidden');
|
||||
if (activeLayout === 'split') splitMock?.classList.remove('hidden');
|
||||
if (activeLayout === 'boxed' || activeLayout === 'asymmetric' || activeLayout === 'single') boxedMock?.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Update colors based on chosen industry
|
||||
function updateIndustryColorRecommendation() {
|
||||
const selectedEl = form.querySelector('input[name="industry"]:checked') as HTMLInputElement;
|
||||
const industryName = selectedEl?.value || 'Schreinerei & Innenausbau';
|
||||
const config = industryColorConfigs[industryName];
|
||||
|
||||
if (config && recLabel && applyRecBtn) {
|
||||
recLabel.textContent = `${config.label} (für ${industryName})`;
|
||||
|
||||
// Remove old click listeners by cloning
|
||||
const newBtn = applyRecBtn.cloneNode(true) as HTMLButtonElement;
|
||||
applyRecBtn.parentNode?.replaceChild(newBtn, applyRecBtn);
|
||||
|
||||
newBtn.addEventListener('click', () => {
|
||||
applyColorCombo(config.primary, config.secondary);
|
||||
});
|
||||
}
|
||||
|
||||
// Update text mockup content in preview
|
||||
const previewText = config ? config.name : 'Premium Web';
|
||||
const previewDescText = `Maßgeschneidertes Design für ${industryName}.`;
|
||||
|
||||
if (previewH1) previewH1.textContent = previewText;
|
||||
if (previewDesc) previewDesc.textContent = previewDescText;
|
||||
if (previewH1Split) previewH1Split.textContent = previewText;
|
||||
if (previewDescSplit) previewDescSplit.textContent = previewDescText;
|
||||
if (previewH1Boxed) previewH1Boxed.textContent = previewText;
|
||||
if (previewDescBoxed) previewDescBoxed.textContent = previewDescText;
|
||||
}
|
||||
|
||||
// Event listener for industry change
|
||||
form.querySelectorAll('input[name="industry"]').forEach(input => {
|
||||
input.addEventListener('change', () => {
|
||||
updateIndustryColorRecommendation();
|
||||
// Automatically pre-apply the recommended combo for the industry
|
||||
const selectedEl = form.querySelector('input[name="industry"]:checked') as HTMLInputElement;
|
||||
const config = industryColorConfigs[selectedEl?.value];
|
||||
if (config) {
|
||||
applyColorCombo(config.primary, config.secondary);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Custom industry input also resets preset if user types
|
||||
if (industryCustomInput) {
|
||||
industryCustomInput.addEventListener('input', () => {
|
||||
if (industryCustomInput.value.trim() && recLabel) {
|
||||
recLabel.textContent = 'Individuelles Thema';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Dynamic Preview Styles (Colors & Themes)
|
||||
function updatePreviewStyles() {
|
||||
if (!previewBox || !previewGlow || !logoDot || !logoText || !badge || !tierBadge) return;
|
||||
|
||||
const primary = primaryColorInput.value;
|
||||
const secondary = secondaryColorInput.value;
|
||||
const activeStyle = (form.querySelector('input[name="style"]:checked') as HTMLInputElement)?.value || 'corporate-dark';
|
||||
|
||||
// Reset style classes
|
||||
previewBox.className = "w-full h-full min-h-[300px] rounded-3xl border p-6 flex flex-col justify-between overflow-hidden relative transition-all duration-500 shadow-xl";
|
||||
logoText.className = "text-[9px] font-bold tracking-tight uppercase";
|
||||
badge.className = "text-[7px] font-bold uppercase tracking-wider px-2 py-0.5 rounded";
|
||||
|
||||
// Color rules for elements
|
||||
logoDot.style.backgroundColor = primary;
|
||||
if (bxAccent) bxAccent.style.backgroundColor = primary;
|
||||
if (s2) {
|
||||
s2.style.borderColor = primary;
|
||||
s2.style.color = primary;
|
||||
}
|
||||
tierBadge.style.color = primary;
|
||||
|
||||
// Style specific themes
|
||||
if (activeStyle === 'corporate-dark') {
|
||||
previewBox.classList.add('bg-[#0B0F19]', 'text-white', 'border-white/10');
|
||||
previewGlow.className = `absolute w-24 h-24 rounded-full blur-2xl pointer-events-none transition-all duration-500 top-1/4 left-1/4`;
|
||||
previewGlow.style.backgroundColor = `${primary}22`;
|
||||
logoText.classList.add('text-white');
|
||||
badge.style.backgroundColor = primary;
|
||||
badge.style.color = '#0B0F19';
|
||||
}
|
||||
else if (activeStyle === 'nordic-light') {
|
||||
previewBox.classList.add('bg-[#F3F4F6]', 'text-slate-900', 'border-slate-200');
|
||||
previewGlow.className = `absolute w-24 h-24 rounded-full blur-2xl pointer-events-none transition-all duration-500 top-1/4 left-1/4`;
|
||||
previewGlow.style.backgroundColor = `${primary}11`;
|
||||
logoText.classList.add('text-slate-800');
|
||||
badge.style.backgroundColor = primary;
|
||||
badge.style.color = '#ffffff';
|
||||
}
|
||||
else if (activeStyle === 'industrial-gold') {
|
||||
previewBox.classList.add('bg-[#1C1D21]', 'text-zinc-100', 'border-zinc-700/50');
|
||||
previewGlow.className = `absolute w-24 h-24 rounded-full blur-2xl pointer-events-none transition-all duration-500 top-1/4 left-1/4`;
|
||||
previewGlow.style.backgroundColor = `${primary}18`;
|
||||
logoText.classList.add('text-zinc-100');
|
||||
badge.style.backgroundColor = primary;
|
||||
badge.style.color = '#1C1D21';
|
||||
}
|
||||
else if (activeStyle === 'brutalism') {
|
||||
previewBox.classList.add('bg-white', 'text-black', 'border-4', 'border-black');
|
||||
previewGlow.className = "hidden";
|
||||
logoText.classList.add('text-black');
|
||||
badge.style.backgroundColor = '#000000';
|
||||
badge.style.color = '#ffffff';
|
||||
badge.classList.add('border', 'border-black');
|
||||
}
|
||||
else if (activeStyle === 'neomorphism') {
|
||||
previewBox.classList.add('bg-[#E0E0E0]', 'text-slate-800', 'border-transparent', 'shadow-[inset_10px_10px_20px_#bebebe,inset_-10px_-10px_20px_#ffffff]');
|
||||
previewGlow.className = "hidden";
|
||||
logoText.classList.add('text-slate-700');
|
||||
badge.style.backgroundColor = primary;
|
||||
badge.style.color = '#white';
|
||||
}
|
||||
else { // saas-minimalism
|
||||
previewBox.classList.add('bg-white', 'text-slate-900', 'border-slate-100', 'shadow-sm');
|
||||
previewGlow.className = `absolute w-24 h-24 rounded-full blur-2xl pointer-events-none transition-all duration-500 top-1/4 left-1/4`;
|
||||
previewGlow.style.backgroundColor = `${primary}08`;
|
||||
logoText.classList.add('text-slate-800');
|
||||
badge.style.backgroundColor = primary;
|
||||
badge.style.color = '#ffffff';
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger initial styles
|
||||
updateIndustryColorRecommendation();
|
||||
updatePreviewLayout();
|
||||
updatePreviewStyles();
|
||||
|
||||
// Steps state machine
|
||||
function updateStep() {
|
||||
steps.forEach((step, idx) => {
|
||||
if (idx + 1 === currentStep) {
|
||||
step.classList.remove('hidden');
|
||||
} else {
|
||||
step.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
dots.forEach((dot, idx) => {
|
||||
if (idx + 1 <= currentStep) {
|
||||
dot.classList.add('bg-sky-500');
|
||||
dot.classList.remove('bg-slate-300', 'dark:bg-zinc-700');
|
||||
} else {
|
||||
dot.classList.remove('bg-sky-500');
|
||||
dot.classList.add('bg-slate-300', 'dark:bg-zinc-700');
|
||||
}
|
||||
});
|
||||
|
||||
if (counter) {
|
||||
counter.textContent = `SCHRITT ${currentStep} / ${totalSteps}`;
|
||||
}
|
||||
|
||||
if (prevBtn && nextBtn) {
|
||||
prevBtn.disabled = currentStep === 1;
|
||||
if (currentStep === totalSteps) {
|
||||
nextBtn.textContent = 'Projekt generieren';
|
||||
} else {
|
||||
nextBtn.textContent = 'Weiter';
|
||||
}
|
||||
}
|
||||
|
||||
if (currentStep === 5) {
|
||||
buildSummary();
|
||||
}
|
||||
}
|
||||
|
||||
function validateStep(): boolean {
|
||||
if (currentStep === 4) {
|
||||
let valid = true;
|
||||
|
||||
if (!nameInput.value.trim()) {
|
||||
nameInput.nextElementSibling?.classList.remove('hidden');
|
||||
valid = false;
|
||||
} else {
|
||||
nameInput.nextElementSibling?.classList.add('hidden');
|
||||
}
|
||||
|
||||
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailPattern.test(emailInput.value)) {
|
||||
emailInput.nextElementSibling?.classList.remove('hidden');
|
||||
valid = false;
|
||||
} else {
|
||||
emailInput.nextElementSibling?.classList.add('hidden');
|
||||
}
|
||||
|
||||
const privacyError = document.getElementById('wizard-privacy')?.closest('label')?.nextElementSibling;
|
||||
if (!privacyInput.checked) {
|
||||
privacyError?.classList.remove('hidden');
|
||||
valid = false;
|
||||
} else {
|
||||
privacyError?.classList.add('hidden');
|
||||
}
|
||||
|
||||
return valid;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function buildSummary() {
|
||||
const selectedIndustryEl = form.querySelector('input[name="industry"]:checked') as HTMLInputElement;
|
||||
const customIndustry = industryCustomInput.value.trim();
|
||||
const industryName = customIndustry ? customIndustry : (selectedIndustryEl ? selectedIndustryEl.value : '-');
|
||||
|
||||
const primary = primaryColorInput.value;
|
||||
const secondary = secondaryColorInput.value;
|
||||
|
||||
const selectedLayoutEl = form.querySelector('input[name="layout"]:checked') as HTMLInputElement;
|
||||
const selectedStyleEl = form.querySelector('input[name="style"]:checked') as HTMLInputElement;
|
||||
|
||||
if (summaryIndustry) summaryIndustry.textContent = industryName;
|
||||
if (summaryColors) summaryColors.textContent = `Primär: ${primary.toUpperCase()}, Sekundär: ${secondary.toUpperCase()}`;
|
||||
if (summaryLayout) summaryLayout.textContent = `${selectedLayoutEl?.value.toUpperCase()} / ${selectedStyleEl?.value.toUpperCase()}`;
|
||||
if (summaryContact) summaryContact.textContent = `${nameInput.value} (${emailInput.value})`;
|
||||
}
|
||||
|
||||
function handleNext() {
|
||||
if (!validateStep()) return;
|
||||
|
||||
if (currentStep < totalSteps) {
|
||||
currentStep++;
|
||||
updateStep();
|
||||
} else {
|
||||
submitWizard();
|
||||
}
|
||||
}
|
||||
|
||||
function submitWizard() {
|
||||
const selectedIndustryEl = form.querySelector('input[name="industry"]:checked') as HTMLInputElement;
|
||||
const customIndustry = industryCustomInput.value.trim();
|
||||
const industryName = customIndustry ? customIndustry : (selectedIndustryEl ? selectedIndustryEl.value : 'unbekannt');
|
||||
|
||||
const selectedLayoutEl = form.querySelector('input[name="layout"]:checked') as HTMLInputElement;
|
||||
const selectedStyleEl = form.querySelector('input[name="style"]:checked') as HTMLInputElement;
|
||||
const companyVal = (document.getElementById('wizard-company') as HTMLInputElement).value;
|
||||
|
||||
const payload = {
|
||||
client_metadata: {
|
||||
agency_id: "nd-it-solutions",
|
||||
timestamp: new Date().toISOString(),
|
||||
domain: companyVal ? `${companyVal.toLowerCase().replace(/[^a-z0-9]/g, '')}.de` : "unbekannt.de"
|
||||
},
|
||||
branding: {
|
||||
industry: industryName,
|
||||
company_name: companyVal || "Mein Unternehmen",
|
||||
branding_style: selectedStyleEl?.value || "corporate-dark",
|
||||
primary_color: primaryColorInput.value,
|
||||
secondary_color: secondaryColorInput.value
|
||||
},
|
||||
structure: {
|
||||
layout_type: selectedLayoutEl?.value || "bento",
|
||||
sections: ["hero", "portfolio", "contact"]
|
||||
},
|
||||
contact: {
|
||||
name: nameInput.value,
|
||||
email: emailInput.value,
|
||||
company: companyVal
|
||||
}
|
||||
};
|
||||
|
||||
console.log('Wizard Payload Output:', JSON.stringify(payload, null, 2));
|
||||
alert('Erfolgreich generiert! JSON Payload wurde in die Konsole geloggt.');
|
||||
}
|
||||
|
||||
if (nextBtn && prevBtn) {
|
||||
nextBtn.addEventListener('click', handleNext);
|
||||
prevBtn.addEventListener('click', () => {
|
||||
if (currentStep > 1) {
|
||||
currentStep--;
|
||||
updateStep();
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@@ -9,12 +9,12 @@
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div class="text-center max-w-3xl mx-auto mb-16">
|
||||
<h2 class="text-xs font-bold tracking-widest text-brand-cyan uppercase mb-3">Unsere Leistungen</h2>
|
||||
<h2 class="text-xs font-bold tracking-widest text-brand-cyan uppercase mb-3">Was wir für Sie tun</h2>
|
||||
<h3 class="text-4xl md:text-5xl font-bold text-brand-primary dark:text-white">
|
||||
Maßgeschneiderte Lösungen für digitale Exzellenz
|
||||
Alles, was Sie für Ihren Erfolg im Internet brauchen
|
||||
</h3>
|
||||
<p class="mt-4 text-lg text-gray-600 dark:text-gray-400">
|
||||
Wir begleiten Ihren Betrieb Schritt für Schritt zur perfekten Website für mehr Kunden und Anfragen.
|
||||
Wir machen es Ihnen einfach. Vom ersten Entwurf bis zur fertigen Seite, die Ihnen neue Kunden bringt.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -33,9 +33,9 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h4 class="text-xl font-bold text-brand-primary dark:text-white mb-2">Firmenwebseiten</h4>
|
||||
<h4 class="text-xl font-bold text-brand-primary dark:text-white mb-2">Digitale Visitenkarte</h4>
|
||||
<p class="text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
|
||||
Individuelle Homepages als digitale Visitenkarte für Ihr Unternehmen, optimiert für alle mobilen Endgeräte.
|
||||
Eine schicke Homepage, auf der alles Wichtige steht: Ihre Öffnungszeiten, Telefonnummer und Ihr Angebot. Läuft perfekt auf Handys.
|
||||
</p>
|
||||
</div>
|
||||
<span class="text-xs font-semibold text-brand-cyan group-hover:text-brand-magenta transition-colors flex items-center gap-1">
|
||||
@@ -45,16 +45,16 @@
|
||||
<!-- Back -->
|
||||
<div class="absolute inset-0 backface-hidden rotate-y-180 bg-gradient-to-br from-brand-cyan/90 to-brand-primary text-white border border-brand-cyan/30 rounded-2xl flex flex-col justify-between p-8 shadow-2xl">
|
||||
<div>
|
||||
<h4 class="text-xl font-bold mb-4">Firmenwebseiten</h4>
|
||||
<h4 class="text-xl font-bold mb-4">Digitale Visitenkarte</h4>
|
||||
<ul class="space-y-2 text-sm text-gray-200">
|
||||
<li class="flex items-center gap-2">✓ Responsive Webdesign (Mobile First)</li>
|
||||
<li class="flex items-center gap-2">✓ Extrem schnelle Ladezeiten</li>
|
||||
<li class="flex items-center gap-2">✓ DSGVO-konform (Cookie-Consent etc.)</li>
|
||||
<li class="flex items-center gap-2">✓ Modernste Web-Tech (Astro & Tailwind)</li>
|
||||
<li class="flex items-center gap-2">✓ Sieht super aus auf jedem Handy</li>
|
||||
<li class="flex items-center gap-2">✓ Lädt blitzschnell ohne Warten</li>
|
||||
<li class="flex items-center gap-2">✓ Sicher vor Abmahnungen (DSGVO)</li>
|
||||
<li class="flex items-center gap-2">✓ Moderne und sichere Technik</li>
|
||||
</ul>
|
||||
</div>
|
||||
<a href="#contact" class="inline-flex items-center justify-center py-2 px-4 bg-white text-brand-primary font-semibold rounded-lg text-xs hover:bg-brand-magenta hover:text-white transition-colors duration-300">
|
||||
Beratung anfordern
|
||||
Kostenlose Beratung anfordern
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -72,9 +72,9 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.042 21.672L13.684 16.6m0 0l-2.51 2.225.569-9.47 7.827 5.176-3.243.681.381 2.29z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h4 class="text-xl font-bold text-brand-primary dark:text-white mb-2">Landingpages</h4>
|
||||
<h4 class="text-xl font-bold text-brand-primary dark:text-white mb-2">Kunden-Gewinnung</h4>
|
||||
<p class="text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
|
||||
Zielgerichtete Webseiten für Google- oder Social-Media-Kampagnen zur effektiven Neukundengewinnung.
|
||||
Eine spezielle Seite, die nur ein Ziel hat: Dass neue Kunden Sie anrufen, Ihnen schreiben oder direkt vorbeikommen.
|
||||
</p>
|
||||
</div>
|
||||
<span class="text-xs font-semibold text-brand-cyan group-hover:text-brand-magenta transition-colors flex items-center gap-1">
|
||||
@@ -84,16 +84,16 @@
|
||||
<!-- Back -->
|
||||
<div class="absolute inset-0 backface-hidden rotate-y-180 bg-gradient-to-br from-purple-900 to-brand-primary text-white border border-brand-magenta/30 rounded-2xl flex flex-col justify-between p-8 shadow-2xl">
|
||||
<div>
|
||||
<h4 class="text-xl font-bold mb-4">Landingpages</h4>
|
||||
<h4 class="text-xl font-bold mb-4">Kunden-Gewinnung</h4>
|
||||
<ul class="space-y-2 text-sm text-gray-200">
|
||||
<li class="flex items-center gap-2">✓ Conversion-Rate Optimierung (CRO)</li>
|
||||
<li class="flex items-center gap-2">✓ Klare Handlungsaufforderungen (CTA)</li>
|
||||
<li class="flex items-center gap-2">✓ A/B Testings & Web-Analysen</li>
|
||||
<li class="flex items-center gap-2">✓ Schnelles Laden für geringe Absprünge</li>
|
||||
<li class="flex items-center gap-2">✓ Leicht verständlich für Besucher</li>
|
||||
<li class="flex items-center gap-2">✓ Telefonnummer direkt im Blick</li>
|
||||
<li class="flex items-center gap-2">✓ Zeigt Ihnen, wer Ihre Seite besucht</li>
|
||||
<li class="flex items-center gap-2">✓ Kunden springen nicht frustriert ab</li>
|
||||
</ul>
|
||||
</div>
|
||||
<a href="#contact" class="inline-flex items-center justify-center py-2 px-4 bg-white text-brand-primary font-semibold rounded-lg text-xs hover:bg-brand-cyan hover:text-brand-primary transition-colors duration-300">
|
||||
Projekt anfragen
|
||||
Gespräch vereinbaren
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,9 +111,9 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 10.5V6a3.75 3.75 0 10-7.5 0v4.5m11.356-1.993l1.263 12c.07.665-.45 1.243-1.119 1.243H4.25a1.125 1.125 0 01-1.12-1.243l1.264-12A1.125 1.125 0 015.513 7.5h12.974c.576 0 1.059.435 1.119 1.007zM8.625 10.5a.375.375 0 11-.75 0 .375.375 0 01.75 0zm7.5 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h4 class="text-xl font-bold text-brand-primary dark:text-white mb-2">Onlineshops</h4>
|
||||
<h4 class="text-xl font-bold text-brand-primary dark:text-white mb-2">Internet-Verkauf</h4>
|
||||
<p class="text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
|
||||
Einfache, performante E-Commerce-Lösungen für Händler, um Produkte online erfolgreich zu vermarkten.
|
||||
Verkaufen Sie Ihre Produkte, Speisen oder Gutscheine direkt online. Kinderleicht für Ihre Kunden zu bedienen.
|
||||
</p>
|
||||
</div>
|
||||
<span class="text-xs font-semibold text-brand-cyan group-hover:text-brand-magenta transition-colors flex items-center gap-1">
|
||||
@@ -123,16 +123,16 @@
|
||||
<!-- Back -->
|
||||
<div class="absolute inset-0 backface-hidden rotate-y-180 bg-gradient-to-br from-brand-magenta/90 to-brand-primary text-white border border-brand-magenta/30 rounded-2xl flex flex-col justify-between p-8 shadow-2xl">
|
||||
<div>
|
||||
<h4 class="text-xl font-bold mb-4">Onlineshops</h4>
|
||||
<h4 class="text-xl font-bold mb-4">Internet-Verkauf</h4>
|
||||
<ul class="space-y-2 text-sm text-gray-200">
|
||||
<li class="flex items-center gap-2">✓ Einfache Produkt- & Bestellpflege</li>
|
||||
<li class="flex items-center gap-2">✓ PayPal, Stripe & Klarna Integration</li>
|
||||
<li class="flex items-center gap-2">✓ Skalierbare E-Commerce-Systeme</li>
|
||||
<li class="flex items-center gap-2">✓ Optimierter Checkout-Prozess</li>
|
||||
<li class="flex items-center gap-2">✓ Produkte & Speisen leicht ändern</li>
|
||||
<li class="flex items-center gap-2">✓ Bezahlung per PayPal oder Karte</li>
|
||||
<li class="flex items-center gap-2">✓ Wächst mit Ihrem Laden mit</li>
|
||||
<li class="flex items-center gap-2">✓ Kinderleichte Bestellung</li>
|
||||
</ul>
|
||||
</div>
|
||||
<a href="#contact" class="inline-flex items-center justify-center py-2 px-4 bg-white text-brand-primary font-semibold rounded-lg text-xs hover:bg-brand-cyan hover:text-brand-primary transition-colors duration-300">
|
||||
Sicherheit prüfen
|
||||
Shop anfragen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -150,9 +150,9 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17L17.25 21A2.652 2.652 0 0021 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.31-.374.711-.6 1.154-.63M11.42 15.17a3 3 0 11-4.088-4.088M12.73 8.21l-.504-.504a1.5 1.5 0 112.122-2.122l.504.504m-2.122 2.122A2.99 2.99 0 0012 8.25m0 0a3.01 3.01 0 01-1.127-.224l-3.23-3.23a1.5 1.5 0 10-2.122 2.122l3.23 3.23C8.63 10.3 8.6 10.65 8.77 11"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h4 class="text-xl font-bold text-brand-primary dark:text-white mb-2">Wartung & Support</h4>
|
||||
<h4 class="text-xl font-bold text-brand-primary dark:text-white mb-2">Technik & Pflege</h4>
|
||||
<p class="text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
|
||||
Wir kümmern uns um Updates, Backups, Sicherheit und inhaltliche Anpassungen Ihrer Website.
|
||||
Sie wollen ein neues Bild zeigen oder die Öffnungszeiten ändern? Schreiben Sie uns einfach. Wir machen das für Sie.
|
||||
</p>
|
||||
</div>
|
||||
<span class="text-xs font-semibold text-brand-cyan group-hover:text-brand-magenta transition-colors flex items-center gap-1">
|
||||
@@ -162,16 +162,16 @@
|
||||
<!-- Back -->
|
||||
<div class="absolute inset-0 backface-hidden rotate-y-180 bg-gradient-to-br from-brand-primary/95 to-purple-950 text-white border border-brand-cyan/20 rounded-2xl flex flex-col justify-between p-8 shadow-2xl">
|
||||
<div>
|
||||
<h4 class="text-xl font-bold mb-4">Wartung & Support</h4>
|
||||
<h4 class="text-xl font-bold mb-4">Technik & Pflege</h4>
|
||||
<ul class="space-y-2 text-sm text-gray-200">
|
||||
<li class="flex items-center gap-2">✓ Regelmäßige System- & Plugin-Updates</li>
|
||||
<li class="flex items-center gap-2">✓ Tägliche Sicherheits-Backups</li>
|
||||
<li class="flex items-center gap-2">✓ Schutz vor Spam & Hackerangriffen</li>
|
||||
<li class="flex items-center gap-2">✓ Schnelle inhaltliche Änderungen</li>
|
||||
<li class="flex items-center gap-2">✓ Wir machen alle wichtigen Updates</li>
|
||||
<li class="flex items-center gap-2">✓ Tägliche automatische Datensicherung</li>
|
||||
<li class="flex items-center gap-2">✓ Schutz vor Internet-Angriffen</li>
|
||||
<li class="flex items-center gap-2">✓ Schnelle Änderungen per WhatsApp/E-Mail</li>
|
||||
</ul>
|
||||
</div>
|
||||
<a href="#contact" class="inline-flex items-center justify-center py-2 px-4 bg-white text-brand-primary font-semibold rounded-lg text-xs hover:bg-brand-magenta hover:text-white transition-colors duration-300">
|
||||
Support anfragen
|
||||
Hilfe anfordern
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -190,9 +190,9 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h4 class="text-xl font-bold text-brand-primary dark:text-white mb-2">Lokale SEO & Google</h4>
|
||||
<h4 class="text-xl font-bold text-brand-primary dark:text-white mb-2">Bei Google gefunden werden</h4>
|
||||
<p class="text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
|
||||
Verbessern Sie Ihre Sichtbarkeit bei lokalen Google-Suchanfragen für mehr Anfragen aus Ihrer Region.
|
||||
Wenn jemand in Ihrer Stadt nach Ihren Angeboten sucht, sorgen wir dafür, dass Sie ganz oben stehen.
|
||||
</p>
|
||||
</div>
|
||||
<span class="text-xs font-semibold text-brand-cyan group-hover:text-brand-magenta transition-colors flex items-center gap-1">
|
||||
@@ -202,16 +202,16 @@
|
||||
<!-- Back -->
|
||||
<div class="absolute inset-0 backface-hidden rotate-y-180 bg-gradient-to-br from-brand-cyan/90 to-purple-950 text-white border border-brand-cyan/30 rounded-2xl flex flex-col justify-between p-8 shadow-2xl">
|
||||
<div>
|
||||
<h4 class="text-xl font-bold mb-4">Lokale SEO</h4>
|
||||
<h4 class="text-xl font-bold mb-4">Bei Google gefunden werden</h4>
|
||||
<ul class="space-y-2 text-sm text-gray-200">
|
||||
<li class="flex items-center gap-2">✓ Google My Business Optimierung</li>
|
||||
<li class="flex items-center gap-2">✓ Keyword-Recherche für Ihre Nische</li>
|
||||
<li class="flex items-center gap-2">✓ OnPage & OffPage Optimierung</li>
|
||||
<li class="flex items-center gap-2">✓ Ladezeiten-Optimierung (Web Vitals)</li>
|
||||
<li class="flex items-center gap-2">✓ Google Maps Eintrag perfekt gepflegt</li>
|
||||
<li class="flex items-center gap-2">✓ Gefunden bei den richtigen Suchwörtern</li>
|
||||
<li class="flex items-center gap-2">✓ Ihre Seite wird für Google optimiert</li>
|
||||
<li class="flex items-center gap-2">✓ Schnelles Laden bringt bessere Plätze</li>
|
||||
</ul>
|
||||
</div>
|
||||
<a href="#contact" class="inline-flex items-center justify-center py-2 px-4 bg-white text-brand-primary font-semibold rounded-lg text-xs hover:bg-brand-magenta hover:text-white transition-colors duration-300">
|
||||
Termin buchen
|
||||
Kostenloses Gespräch vereinbaren
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -229,9 +229,9 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.076-3.077a2.25 2.25 0 00-3.182-3.182L13.1 7.27a15.996 15.996 0 00-4.648 4.764m3.122 3.122l-1.22 4.88m0 0a3 3 0 00-5.78 1.128c0 .285.023.57.067.85"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h4 class="text-xl font-bold text-brand-primary dark:text-white mb-2">UI/UX Webdesign</h4>
|
||||
<h4 class="text-xl font-bold text-brand-primary dark:text-white mb-2">Schönes Aussehen</h4>
|
||||
<p class="text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
|
||||
Individuelle Design-Entwürfe, die auf Ihre Markenidentität abgestimmt sind und Besucher begeistern.
|
||||
Wir gestalten Ihre Webseite so, dass sie modern, einladend und genau passend zu Ihrem Geschäft aussieht.
|
||||
</p>
|
||||
</div>
|
||||
<span class="text-xs font-semibold text-brand-cyan group-hover:text-brand-magenta transition-colors flex items-center gap-1">
|
||||
@@ -241,16 +241,16 @@
|
||||
<!-- Back -->
|
||||
<div class="absolute inset-0 backface-hidden rotate-y-180 bg-gradient-to-br from-brand-magenta/80 to-brand-primary text-white border border-brand-magenta/20 rounded-2xl flex flex-col justify-between p-8 shadow-2xl">
|
||||
<div>
|
||||
<h4 class="text-xl font-bold mb-4">Webdesign</h4>
|
||||
<h4 class="text-xl font-bold mb-4">Schönes Aussehen</h4>
|
||||
<ul class="space-y-2 text-sm text-gray-200">
|
||||
<li class="flex items-center gap-2">✓ Einzigartige, moderne Designs</li>
|
||||
<li class="flex items-center gap-2">✓ Abgestimmtes Farb- & Schriftkonzept</li>
|
||||
<li class="flex items-center gap-2">✓ Intuitive Benutzerführung (UI/UX)</li>
|
||||
<li class="flex items-center gap-2">✓ Erstellung von Figma-Prototypen</li>
|
||||
<li class="flex items-center gap-2">✓ Kein langweiliges Standard-Design</li>
|
||||
<li class="flex items-center gap-2">✓ Schöne Farben passend zu Ihrem Logo</li>
|
||||
<li class="flex items-center gap-2">✓ Kunden finden sofort, was sie suchen</li>
|
||||
<li class="flex items-center gap-2">✓ Sie sehen das Design vor dem Start</li>
|
||||
</ul>
|
||||
</div>
|
||||
<a href="#contact" class="inline-flex items-center justify-center py-2 px-4 bg-white text-brand-primary font-semibold rounded-lg text-xs hover:bg-brand-cyan hover:text-brand-primary transition-colors duration-300">
|
||||
Potenziale besprechen
|
||||
Design besprechen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
112
src/components/admin/AdminSidebar.astro
Normal file
112
src/components/admin/AdminSidebar.astro
Normal file
@@ -0,0 +1,112 @@
|
||||
---
|
||||
// src/components/admin/AdminSidebar.astro
|
||||
const { currentPath = '' } = Astro.props;
|
||||
|
||||
const navItems = [
|
||||
{ href: '/admin', label: 'Allgemein & Konfig', icon: '🎛️' },
|
||||
{ href: '/admin/pages', label: 'Seiten-Verwaltung', icon: '📄' },
|
||||
{ href: '/admin/settings', label: 'Stammdaten & SEO', icon: '⚙️' },
|
||||
];
|
||||
|
||||
---
|
||||
|
||||
<!-- Burger Button (Mobile / Sidebar Toggle) -->
|
||||
<button
|
||||
id="admin-menu-toggle"
|
||||
type="button"
|
||||
aria-label="Admin Menü öffnen"
|
||||
class="fixed top-3 left-3 z-50 p-2.5 rounded-xl bg-slate-900/90 border border-white/10 text-white hover:border-sky-400 focus:outline-none focus:ring-2 focus:ring-sky-400 backdrop-blur-md transition-all"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Backdrop Overlay for Mobile -->
|
||||
<div
|
||||
id="admin-sidebar-backdrop"
|
||||
class="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-40 hidden transition-opacity"
|
||||
></div>
|
||||
|
||||
<!-- Drawer Sidebar -->
|
||||
<aside
|
||||
id="admin-sidebar"
|
||||
class="fixed top-0 left-0 h-full w-72 bg-slate-950 border-r border-white/10 p-6 z-50 transform -translate-x-full transition-transform duration-300 ease-in-out flex flex-col justify-between shadow-2xl"
|
||||
>
|
||||
<div class="space-y-6">
|
||||
<!-- Header with Close Button -->
|
||||
<div class="flex items-center justify-between border-b border-white/10 pb-4">
|
||||
<div>
|
||||
<span class="text-[10px] font-mono text-sky-400 uppercase tracking-widest block">N&D WaaS Admin</span>
|
||||
<h2 class="text-lg font-bold text-white">Navigation</h2>
|
||||
</div>
|
||||
<button
|
||||
id="admin-menu-close"
|
||||
type="button"
|
||||
class="p-2 text-slate-400 hover:text-white rounded-lg hover:bg-slate-900 transition-colors"
|
||||
aria-label="Menü schließen"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Links -->
|
||||
<nav class="space-y-1.5" aria-label="Admin Sidebar">
|
||||
{navItems.map((item) => {
|
||||
const isActive = currentPath === item.href || (item.href !== '/admin' && currentPath.startsWith(item.href));
|
||||
return (
|
||||
<a
|
||||
href={item.href}
|
||||
class={`flex items-center gap-3 px-4 py-3 rounded-xl text-xs font-semibold transition-all ${
|
||||
isActive
|
||||
? 'bg-sky-400/10 text-sky-400 border border-sky-400/30'
|
||||
: 'text-slate-300 hover:bg-slate-900 hover:text-white border border-transparent'
|
||||
}`}
|
||||
>
|
||||
<span class="text-base">{item.icon}</span>
|
||||
<span>{item.label}</span>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Quick Links -->
|
||||
<div class="pt-4 border-t border-white/10 space-y-2">
|
||||
<a
|
||||
href="/?preview=draft"
|
||||
target="_blank"
|
||||
class="flex items-center justify-center gap-2 w-full p-2.5 rounded-xl bg-amber-500/10 text-amber-400 border border-amber-500/20 text-xs font-semibold hover:bg-amber-500 hover:text-slate-950 transition-all"
|
||||
>
|
||||
🛠️ Entwurf-Vorschau
|
||||
</a>
|
||||
<a
|
||||
href="/"
|
||||
target="_blank"
|
||||
class="flex items-center justify-center gap-2 w-full p-2.5 rounded-xl bg-slate-900 text-slate-300 border border-white/10 text-xs font-semibold hover:border-sky-400 hover:text-white transition-all"
|
||||
>
|
||||
🌐 Live-Website
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<script>
|
||||
const toggleBtn = document.getElementById('admin-menu-toggle');
|
||||
const closeBtn = document.getElementById('admin-menu-close');
|
||||
const sidebar = document.getElementById('admin-sidebar');
|
||||
const backdrop = document.getElementById('admin-sidebar-backdrop');
|
||||
|
||||
function openMenu() {
|
||||
sidebar?.classList.remove('-translate-x-full');
|
||||
backdrop?.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
sidebar?.classList.add('-translate-x-full');
|
||||
backdrop?.classList.add('hidden');
|
||||
}
|
||||
|
||||
toggleBtn?.addEventListener('click', openMenu);
|
||||
closeBtn?.addEventListener('click', closeMenu);
|
||||
backdrop?.addEventListener('click', closeMenu);
|
||||
</script>
|
||||
41
src/components/sections/BentoGrid.astro
Normal file
41
src/components/sections/BentoGrid.astro
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
import type { SiteConfig } from '../../utils/configLoader';
|
||||
|
||||
interface Props {
|
||||
config: SiteConfig['bento'];
|
||||
}
|
||||
|
||||
const props = Astro.props;
|
||||
const config = props.config || props.settings || props;
|
||||
const sectionTitle = config.sectionTitle || config.title || 'Unsere Leistungen';
|
||||
const sectionSubtitle = config.sectionSubtitle || config.subtitle || '';
|
||||
const cards = config.cards || config.items || [];
|
||||
---
|
||||
|
||||
<section id="services" class={`py-20 px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto relative ${config.animation ? `anim-${config.animation}` : ''}`} style={(config as any).bg_color ? `background-color: ${(config as any).bg_color};` : ''}>
|
||||
|
||||
<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">{sectionTitle}</h2>
|
||||
<p class="text-slate-400 text-xs sm:text-sm">{sectionSubtitle}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{cards.map((card: any) => (
|
||||
<div class={`relative glass-panel glass-panel-hover rounded-2xl p-8 flex flex-col justify-between overflow-hidden group ${card.colSpan === 2 ? 'md:col-span-2' : 'md:col-span-1'} ${card.animation ? `anim-${card.animation}` : ''}`}>
|
||||
<div class="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>
|
||||
93
src/components/sections/ContactSection.astro
Normal file
93
src/components/sections/ContactSection.astro
Normal file
@@ -0,0 +1,93 @@
|
||||
---
|
||||
import type { SiteConfig, SmtpConfig } from '../../utils/configLoader';
|
||||
|
||||
interface Props {
|
||||
contactConfig: SiteConfig['contact'];
|
||||
smtpConfig: SmtpConfig;
|
||||
}
|
||||
|
||||
const props = Astro.props;
|
||||
const contactConfig = props.contactConfig || props.settings || props;
|
||||
const smtpConfig = props.smtpConfig || { isConfigured: false };
|
||||
const title = contactConfig.title || 'Kontaktieren Sie uns';
|
||||
const subtitle = contactConfig.subtitle || 'Wir freuen uns auf Ihre Nachricht.';
|
||||
---
|
||||
|
||||
<section id="contact" class="py-20 px-4 sm:px-6 lg:px-8 max-w-4xl mx-auto relative" style={(contactConfig as any).bg_color ? `background-color: ${(contactConfig as any).bg_color};` : ''}>
|
||||
|
||||
<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">{title}</h2>
|
||||
<p class="text-slate-400 text-xs sm:text-sm">{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>
|
||||
15
src/components/sections/FallbackSection.astro
Normal file
15
src/components/sections/FallbackSection.astro
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
interface Props {
|
||||
sectionName?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
const { sectionName = 'Unbekannt', message } = 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>⚙️ {message || `Section ${sectionName} ist noch in Bearbeitung oder Platzhalter.`}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
85
src/components/sections/HeroSection.astro
Normal file
85
src/components/sections/HeroSection.astro
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
import type { SiteConfig, UIComponent } from '../../utils/configLoader';
|
||||
import { resolveDynamicBinding } from '../../utils/dynamicDataBinding';
|
||||
|
||||
interface Props {
|
||||
config: SiteConfig['hero'];
|
||||
contextData?: Record<string, any>;
|
||||
}
|
||||
|
||||
const props = Astro.props;
|
||||
const config = props.config || props.settings || props;
|
||||
const contextData = props.contextData || {};
|
||||
|
||||
const heroTitle = resolveDynamicBinding(config.title || config.heroTitle || 'Willkommen', contextData);
|
||||
const heroSubtitle = resolveDynamicBinding(config.subtitle || config.heroSubtitle || '', contextData);
|
||||
const heroBadge = resolveDynamicBinding(config.badge || config.heroBadge || '', contextData);
|
||||
|
||||
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 ${config.animation ? `anim-${config.animation}` : ''}`} style={config.bg_color ? `background-color: ${config.bg_color};` : ''}>
|
||||
<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">
|
||||
{heroBadge && (
|
||||
<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>{heroBadge}</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.animation0 ? `anim-${el.animation0}` : ''}`}>
|
||||
{resolveDynamicBinding(el.content, contextData)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h1 class="text-4xl sm:text-6xl font-extrabold tracking-tight text-white leading-tight">
|
||||
{heroTitle}
|
||||
</h1>
|
||||
|
||||
<p class="text-slate-400 text-xs sm:text-lg max-w-2xl mx-auto leading-relaxed font-normal">
|
||||
{heroSubtitle}
|
||||
</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">
|
||||
{resolveDynamicBinding(config.ctaPrimaryText, contextData)}
|
||||
</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">
|
||||
{resolveDynamicBinding(config.ctaSecondaryText, contextData)}
|
||||
</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.animation0 ? `anim-${el.animation0}` : ''}`}>
|
||||
{resolveDynamicBinding(el.content, contextData)}
|
||||
</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>
|
||||
{resolveDynamicBinding(el.content, contextData)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</section>
|
||||
109
src/db/index.ts
Normal file
109
src/db/index.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||
const DB_PATH = path.join(DATA_DIR, 'waas.db');
|
||||
|
||||
let dbInstance: DatabaseSync | null = null;
|
||||
|
||||
export function getDb(): DatabaseSync {
|
||||
if (dbInstance) return dbInstance;
|
||||
|
||||
if (!fs.existsSync(DATA_DIR)) {
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
dbInstance = new DatabaseSync(DB_PATH);
|
||||
initSchema(dbInstance);
|
||||
return dbInstance;
|
||||
}
|
||||
|
||||
function initSchema(db: DatabaseSync) {
|
||||
// 1. site_settings
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS site_settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value_json TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
// 2. pages
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS pages (
|
||||
id TEXT PRIMARY KEY,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
title TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'published',
|
||||
homepage_id TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
// 3. page_sections
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS page_sections (
|
||||
id TEXT PRIMARY KEY,
|
||||
page_id TEXT NOT NULL,
|
||||
section_type TEXT NOT NULL,
|
||||
order_index INTEGER NOT NULL DEFAULT 0,
|
||||
content_draft_json TEXT NOT NULL,
|
||||
content_published_json TEXT NOT NULL,
|
||||
styles_json TEXT,
|
||||
FOREIGN KEY (page_id) REFERENCES pages (id) ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
|
||||
// Initial Seeding falls DB leer
|
||||
seedInitialData(db);
|
||||
}
|
||||
|
||||
function seedInitialData(db: DatabaseSync) {
|
||||
const checkPages = db.prepare('SELECT COUNT(*) as count FROM pages').get() as { count: number };
|
||||
if (checkPages.count > 0) return;
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
// Seed site_settings
|
||||
const initialSettings = {
|
||||
siteName: 'N&D IT Solutions',
|
||||
metaDescription: 'High-Performance Webdesign & Digital Solutions - 100% DSGVO-konform.',
|
||||
is_demo_mode: false,
|
||||
header: {
|
||||
brandName: 'N&D',
|
||||
brandHighlight: 'i-t SOLUTIONS',
|
||||
ctaText: 'Erstgespräch buchen',
|
||||
ctaLink: '#contact',
|
||||
showThemeToggle: true,
|
||||
glassEffect: true
|
||||
},
|
||||
footer: {
|
||||
brandDescription: 'Ihr Partner für professionelles Webdesign, Website-Entwicklung und lokale SEO.',
|
||||
copyrightText: 'N&D i-t SOLUTIONS. Alle Rechte vorbehalten.',
|
||||
showLegalLinks: true
|
||||
},
|
||||
site_info: {
|
||||
title: 'N&D IT Solutions',
|
||||
homepage_id: 'page_home'
|
||||
}
|
||||
};
|
||||
|
||||
db.prepare('INSERT INTO site_settings (key, value_json, updated_at) VALUES (?, ?, ?)').run(
|
||||
'site_config',
|
||||
JSON.stringify(initialSettings),
|
||||
now
|
||||
);
|
||||
|
||||
// Seed default page only (no prefilled sections/demo content)
|
||||
db.prepare('INSERT INTO pages (id, slug, title, status, homepage_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)').run(
|
||||
'page_home',
|
||||
'/',
|
||||
'Startseite',
|
||||
'published',
|
||||
'page_home',
|
||||
now,
|
||||
now
|
||||
);
|
||||
}
|
||||
33
src/db/resetDb.ts
Normal file
33
src/db/resetDb.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||
const DB_PATH = path.join(DATA_DIR, 'waas.db');
|
||||
|
||||
export function resetDatabaseToBlankSlate(): void {
|
||||
const db = new DatabaseSync(DB_PATH);
|
||||
|
||||
// Lösche bestehende Seiten & Sektionen
|
||||
db.exec('DELETE FROM page_sections');
|
||||
db.exec('DELETE FROM pages');
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
// Leere Startseite anlegen (ohne vordefinierte Sektionen/Inhalte)
|
||||
db.prepare('INSERT INTO pages (id, slug, title, status, homepage_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)').run(
|
||||
'page_home',
|
||||
'/',
|
||||
'Startseite',
|
||||
'published',
|
||||
'page_home',
|
||||
now,
|
||||
now
|
||||
);
|
||||
|
||||
console.log('[WaaS DB] Datenbank auf leeren Stand (Blank Slate) zurückgesetzt.');
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith('resetDb.ts')) {
|
||||
resetDatabaseToBlankSlate();
|
||||
}
|
||||
@@ -8,13 +8,25 @@ 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;
|
||||
ogImage?: 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(Astro.request.headers.get('user-agent') ?? '');
|
||||
const activePreset = THEME_PRESETS[themeConfig.activePreset] || THEME_PRESETS.corporateDark;
|
||||
const colors = {
|
||||
...activePreset.colors,
|
||||
...(themeConfig.customColors || {})
|
||||
};
|
||||
|
||||
const { title, description = "Professionelles Webdesign & moderne Websites.", ogImage } = Astro.props;
|
||||
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
@@ -25,22 +37,57 @@ const { title, description = "Professionelles Webdesign & moderne Websites für
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>{title}</title>
|
||||
|
||||
<script is:inline src="/js/editor-bridge.js"></script>
|
||||
|
||||
<!-- 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.85);
|
||||
--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">
|
||||
{(siteConfig as any).is_demo_mode && (
|
||||
<div class="bg-gradient-to-r from-amber-600 via-amber-500 to-amber-600 text-slate-950 text-center py-2 px-4 text-xs font-bold font-mono tracking-wide shadow-md flex items-center justify-center gap-2">
|
||||
<span>🎭 DEMO-MODUS AKTIV</span>
|
||||
<span class="opacity-75 font-sans font-normal">— Dies ist eine interaktive Vorschau-Instanz der N&D WaaS Engine.</span>
|
||||
</div>
|
||||
)}
|
||||
<slot />
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
<style is:global>
|
||||
|
||||
25
src/pages/404.astro
Normal file
25
src/pages/404.astro
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
// src/pages/404.astro
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
---
|
||||
|
||||
<Layout title="404 - Seite nicht gefunden | N&D IT Solutions">
|
||||
<main class="min-h-screen bg-[#0B0F19] text-white flex items-center justify-center p-4">
|
||||
<div class="preset-card p-10 bg-slate-900/80 border border-white/10 rounded-2xl max-w-lg w-full text-center space-y-6 shadow-2xl backdrop-blur-md">
|
||||
<span class="text-6xl font-extrabold text-sky-400 font-mono tracking-widest block">404</span>
|
||||
<h1 class="text-2xl font-bold text-white">Seite nicht gefunden</h1>
|
||||
<p class="text-slate-400 text-sm leading-relaxed">
|
||||
Die von Ihnen angeforderte Seite existiert nicht oder wurde verschoben.
|
||||
</p>
|
||||
|
||||
<div class="pt-4">
|
||||
<a
|
||||
href="/"
|
||||
class="inline-block bg-sky-500 hover:bg-sky-400 text-slate-950 font-bold px-6 py-3 rounded-xl text-sm transition-all shadow-lg shadow-sky-500/20 active:scale-95"
|
||||
>
|
||||
← Zurück zur Startseite
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</Layout>
|
||||
117
src/pages/[...slug].astro
Normal file
117
src/pages/[...slug].astro
Normal file
@@ -0,0 +1,117 @@
|
||||
---
|
||||
// src/pages/[...slug].astro
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import { loadWaasConfigs } from '../utils/configLoader';
|
||||
import { resolveDynamicBinding } from '../utils/dynamicDataBinding';
|
||||
|
||||
// Component Registry Import (Zero-Sumpf & Modular)
|
||||
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';
|
||||
|
||||
// Strikter SSR-Modus für Instant-Updates ohne Re-Build
|
||||
export const prerender = false;
|
||||
|
||||
// 1. Slug aus der URL auslesen
|
||||
const { slug } = Astro.params;
|
||||
|
||||
// 2. Config aus /app/data/site.config.json oder site.config.draft.json laden
|
||||
const userAgent = Astro.request.headers.get('user-agent') ?? '';
|
||||
const isDraft = Astro.url.searchParams.get('preview') === 'draft';
|
||||
const { siteConfig } = loadWaasConfigs(userAgent, isDraft);
|
||||
|
||||
// Helper function to resolve dynamic data recursively in settings or objects
|
||||
function resolveDynamicData(settings: any, dynamicBindings: any, config: any): any {
|
||||
if (!settings) return settings;
|
||||
const context = { site: config, ...config };
|
||||
|
||||
if (typeof settings === 'string') {
|
||||
return resolveDynamicBinding(settings, context);
|
||||
}
|
||||
|
||||
if (Array.isArray(settings)) {
|
||||
return settings.map((item) => resolveDynamicData(item, dynamicBindings, config));
|
||||
}
|
||||
|
||||
if (typeof settings === 'object') {
|
||||
const resolved: Record<string, any> = {};
|
||||
for (const [key, value] of Object.entries(settings)) {
|
||||
resolved[key] = resolveDynamicData(value, dynamicBindings, config);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
// 3. Normalized Slug-Handling: "/" für Startseite, sonst z.B. "leistungen"
|
||||
const currentSlug = slug === undefined || slug === '' ? '/' : slug.replace(/\/$/, '');
|
||||
|
||||
// 4. Richtige Seite im pages-Array ermitteln (falls vorhanden, sonst Fallback auf Hauptkonfiguration)
|
||||
const pages = (siteConfig as any).pages || [
|
||||
{
|
||||
id: (siteConfig as any).homepage_id || 'home',
|
||||
slug: '/',
|
||||
title: siteConfig.siteName,
|
||||
is_published: true,
|
||||
sections: siteConfig.sectionsOrder.map((secType: string) => ({
|
||||
id: secType,
|
||||
type: secType,
|
||||
settings: (siteConfig as any)[secType] || {}
|
||||
}))
|
||||
}
|
||||
];
|
||||
|
||||
let page = pages.find((p: any) => {
|
||||
if (currentSlug === '/') {
|
||||
// Startseiten-Mapping über homepage_id oder Slug "/"
|
||||
return p.id === (siteConfig as any).homepage_id || p.slug === '/' || p.slug === '';
|
||||
}
|
||||
return p.slug === currentSlug || p.slug === `/${currentSlug}`;
|
||||
});
|
||||
|
||||
// 5. Zero-Crash Fallback Guard: Wenn Seite nicht existiert oder nicht veröffentlicht ist -> 404 Status
|
||||
if (!page || (page.is_published === false && !isDraft)) {
|
||||
Astro.response.status = 404;
|
||||
return Astro.rewrite('/404');
|
||||
}
|
||||
|
||||
|
||||
// 6. Dynamic Component Registry Mapping
|
||||
const ComponentRegistry: Record<string, any> = {
|
||||
hero: HeroSection,
|
||||
bento: BentoGrid,
|
||||
contact: ContactSection,
|
||||
HeroSection,
|
||||
BentoGrid,
|
||||
ContactSection
|
||||
};
|
||||
---
|
||||
|
||||
<Layout
|
||||
title={page.title || siteConfig.siteName || 'N&D IT Solutions'}
|
||||
description={page.seo?.description || siteConfig.metaDescription || ''}
|
||||
ogImage={page.seo?.og_image || ''}
|
||||
>
|
||||
<main id="main-content" role="main" class="min-h-screen">
|
||||
{
|
||||
page.sections && page.sections.length > 0 ? (
|
||||
page.sections.map((section: any) => {
|
||||
const Component = ComponentRegistry[section.type] || FallbackSection;
|
||||
|
||||
// Löse Platzhalter & DB-Bindings aus /admin/ live auf
|
||||
const resolvedSettings = resolveDynamicData(
|
||||
section.settings || {},
|
||||
section.dynamic_bindings || {},
|
||||
siteConfig
|
||||
);
|
||||
|
||||
return <Component id={section.id} settings={resolvedSettings} {...resolvedSettings} />;
|
||||
})
|
||||
) : (
|
||||
<FallbackSection message="Diese Seite enthält aktuell noch keine Abschnitte." />
|
||||
)
|
||||
}
|
||||
</main>
|
||||
</Layout>
|
||||
981
src/pages/admin/editor.astro
Normal file
981
src/pages/admin/editor.astro
Normal file
@@ -0,0 +1,981 @@
|
||||
---
|
||||
// src/pages/admin/editor.astro
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import AdminSidebar from "../../components/admin/AdminSidebar.astro";
|
||||
import { loadWaasConfigs } from "../../utils/configLoader";
|
||||
import { THEME_PRESETS } from "../../utils/themePresets";
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
const { searchParams } = Astro.url;
|
||||
const pageId = searchParams.get("page_id");
|
||||
|
||||
// Lade den aktuellen Entwurf oder Fallback auf Live Config
|
||||
const userAgent = Astro.request.headers.get("user-agent") ?? "";
|
||||
const { siteConfig: draftConfig } = loadWaasConfigs(userAgent, true);
|
||||
const pages = (draftConfig as any).pages || [];
|
||||
const targetPage = pages.find((p: any) => p.id === pageId) ||
|
||||
pages[0] || { id: "page_home", title: "Startseite", slug: "/", sections: [] };
|
||||
const previewUrl = `${targetPage?.slug || "/"}?preview=draft`;
|
||||
---
|
||||
|
||||
<Layout title={`Editor: ${targetPage?.title || "Baukasten"} | N&D Admin`}>
|
||||
<AdminSidebar currentPath="/admin/editor" />
|
||||
|
||||
<!-- Main Container mit Elementor Dark Theme UI (#1e1e1e, #262626, Elementor Pink #e0436b & Sky Blue #38bdf8) -->
|
||||
<div
|
||||
class="h-screen w-screen flex overflow-hidden bg-[#121212] font-sans pl-12 text-slate-200 selection:bg-[#e0436b] selection:text-white"
|
||||
>
|
||||
<!-- LINKES ELEMENTOR PANEL (360px - 400px) -->
|
||||
<aside
|
||||
id="elementor-panel"
|
||||
class="w-[380px] min-w-[380px] max-w-[380px] bg-[#1e1e1e] border-r border-[#2d2d2d] flex flex-col justify-between z-30 shadow-2xl relative"
|
||||
>
|
||||
<!-- 1. HEADER LEISTE -->
|
||||
<div
|
||||
class="h-12 bg-[#181818] border-b border-[#2d2d2d] flex items-center justify-between px-3"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<a
|
||||
href="/admin/pages"
|
||||
class="p-1.5 rounded hover:bg-[#2a2a2a] text-slate-400 hover:text-white transition-colors"
|
||||
title="Zurück zum Admin-Dashboard"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16"></path></svg
|
||||
>
|
||||
</a>
|
||||
<button
|
||||
id="btn-open-widgets"
|
||||
class="p-1.5 rounded hover:bg-[#2a2a2a] text-[#e0436b] transition-colors"
|
||||
title="Widget-Bibliothek"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"
|
||||
><path
|
||||
d="M4 4h4v4H4V4zm6 0h4v4h-4V4zm6 0h4v4h-4V4zM4 10h4v4H4v-4zm6 0h4v4h-4v-4zm6 0h4v4h-4v-4zM4 16h4v4H4v-4zm6 0h4v4h-4v-4zm6 0h4v4h-4v-4z"
|
||||
></path></svg
|
||||
>
|
||||
</button>
|
||||
<select
|
||||
id="editor-page-switcher"
|
||||
class="bg-[#262626] border border-[#333] hover:border-[#38bdf8] text-xs font-bold text-slate-200 rounded px-2 py-1 outline-none transition-colors max-w-[160px] cursor-pointer"
|
||||
title="Seite im Editor wechseln"
|
||||
>
|
||||
{pages.map((p: any) => (
|
||||
<option value={p.id} selected={p.id === targetPage.id}>
|
||||
{p.title} ({p.slug})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<span
|
||||
id="inspector-active-badge"
|
||||
class="hidden text-[10px] font-mono font-semibold px-2 py-0.5 rounded bg-[#e0436b]/20 text-[#e0436b] border border-[#e0436b]/30"
|
||||
>
|
||||
Inspector
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. HAUPTBEREICH (DYNAMISCHES PANEL) -->
|
||||
<div
|
||||
class="flex-1 overflow-y-auto custom-scrollbar relative bg-[#1e1e1e]"
|
||||
>
|
||||
<!-- STATE A: WIDGET-DRAWER (Katalog & Sektions-Übersicht) -->
|
||||
<div id="panel-state-widgets" class="p-4 space-y-6">
|
||||
<!-- Sektionen verwalten -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h2
|
||||
class="text-xs font-bold uppercase tracking-wider text-slate-400"
|
||||
>
|
||||
Sektionen auf Seite
|
||||
</h2>
|
||||
<span
|
||||
class="text-[10px] bg-[#2a2a2a] text-slate-400 px-2 py-0.5 rounded font-mono"
|
||||
>{targetPage?.sections?.length || 0}</span
|
||||
>
|
||||
</div>
|
||||
<div id="section-list" class="space-y-2">
|
||||
{
|
||||
targetPage?.sections?.map((sec: any, index: number) => (
|
||||
<div
|
||||
class="group p-3 bg-[#262626] hover:bg-[#2d2d2d] border border-[#333] hover:border-[#e0436b]/50 rounded-lg text-xs flex items-center justify-between transition-all cursor-pointer shadow-sm"
|
||||
data-section-id={sec.id}
|
||||
data-section-type={sec.type}
|
||||
data-settings={JSON.stringify(sec.settings || {})}
|
||||
>
|
||||
<div class="flex items-center gap-2.5 min-w-0 flex-1">
|
||||
<span class="text-[10px] font-mono text-[#e0436b] font-bold">
|
||||
#{index + 1}
|
||||
</span>
|
||||
<span class="font-semibold text-slate-200 truncate">
|
||||
{sec.type}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 opacity-80 group-hover:opacity-100">
|
||||
<button
|
||||
class="btn-inspect-section p-1 hover:text-[#38bdf8] transition-colors"
|
||||
title="Im 3-Tab-Inspector öffnen"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
<button
|
||||
class="btn-move-up p-1 hover:text-[#38bdf8] transition-colors"
|
||||
title="Nach oben"
|
||||
>
|
||||
⬆️
|
||||
</button>
|
||||
<button
|
||||
class="btn-move-down p-1 hover:text-[#38bdf8] transition-colors"
|
||||
title="Nach unten"
|
||||
>
|
||||
⬇️
|
||||
</button>
|
||||
<button
|
||||
class="btn-delete-section p-1 hover:text-red-400 transition-colors"
|
||||
title="Sektion löschen"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sektion / Widget hinzufügen -->
|
||||
<div class="pt-4 border-t border-[#2d2d2d] space-y-3">
|
||||
<h2
|
||||
class="text-xs font-bold uppercase tracking-wider text-slate-400"
|
||||
>
|
||||
Widget-Bibliothek (Neue Sektion)
|
||||
</h2>
|
||||
<div class="grid grid-cols-2 gap-2.5">
|
||||
<button
|
||||
data-widget-type="HeroSection"
|
||||
class="btn-quick-add-widget p-3 bg-[#262626] hover:bg-[#2d2d2d] border border-[#333] hover:border-[#38bdf8] rounded-xl flex flex-col items-center gap-2 text-center transition-all group"
|
||||
>
|
||||
<span class="text-xl group-hover:scale-110 transition-transform"
|
||||
>🚀</span
|
||||
>
|
||||
<span class="text-xs font-bold text-slate-200"
|
||||
>Hero Section</span
|
||||
>
|
||||
</button>
|
||||
<button
|
||||
data-widget-type="BentoGrid"
|
||||
class="btn-quick-add-widget p-3 bg-[#262626] hover:bg-[#2d2d2d] border border-[#333] hover:border-[#38bdf8] rounded-xl flex flex-col items-center gap-2 text-center transition-all group"
|
||||
>
|
||||
<span class="text-xl group-hover:scale-110 transition-transform"
|
||||
>🍱</span
|
||||
>
|
||||
<span class="text-xs font-bold text-slate-200">Bento Grid</span>
|
||||
</button>
|
||||
<button
|
||||
data-widget-type="ContactSection"
|
||||
class="btn-quick-add-widget p-3 bg-[#262626] hover:bg-[#2d2d2d] border border-[#333] hover:border-[#38bdf8] rounded-xl flex flex-col items-center gap-2 text-center transition-all group"
|
||||
>
|
||||
<span class="text-xl group-hover:scale-110 transition-transform"
|
||||
>📬</span
|
||||
>
|
||||
<span class="text-xs font-bold text-slate-200"
|
||||
>Kontakt Formular</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STATE B: 3-TAB-INSPEKTORMENÜ (Elementor Inspector) -->
|
||||
<div id="panel-state-inspector" class="hidden flex flex-col h-full">
|
||||
<!-- TAB HEADER -->
|
||||
<div class="flex border-b border-[#2d2d2d] bg-[#181818]">
|
||||
<button
|
||||
id="tab-btn-content"
|
||||
class="flex-1 py-3 text-xs font-bold text-[#e0436b] border-b-2 border-[#e0436b] transition-all flex items-center justify-center gap-1.5"
|
||||
>
|
||||
<span>📝</span> Inhalt
|
||||
</button>
|
||||
<button
|
||||
id="tab-btn-style"
|
||||
class="flex-1 py-3 text-xs font-bold text-slate-400 hover:text-slate-200 border-b-2 border-transparent transition-all flex items-center justify-center gap-1.5"
|
||||
>
|
||||
<span>🎨</span> Stil
|
||||
</button>
|
||||
<button
|
||||
id="tab-btn-advanced"
|
||||
class="flex-1 py-3 text-xs font-bold text-slate-400 hover:text-slate-200 border-b-2 border-transparent transition-all flex items-center justify-center gap-1.5"
|
||||
>
|
||||
<span>⚙️</span> Erweitert
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- TAB INHALTE -->
|
||||
<form
|
||||
id="form-elementor-inspector"
|
||||
class="p-4 flex-1 space-y-6 overflow-y-auto"
|
||||
>
|
||||
<input type="hidden" id="active-section-id" name="section_id" />
|
||||
|
||||
<!-- TAB 1: INHALT -->
|
||||
<div id="tab-panel-content" class="space-y-4">
|
||||
<div
|
||||
class="flex items-center justify-between pb-2 border-b border-[#2d2d2d]"
|
||||
>
|
||||
<span class="text-xs font-bold text-slate-300"
|
||||
>Text- & Medienfelder</span
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
id="btn-[#dynamic-picker-trigger]"
|
||||
class="text-[10px] font-mono text-[#38bdf8] bg-[#38bdf8]/10 hover:bg-[#38bdf8]/20 px-2 py-0.5 rounded border border-[#38bdf8]/30 flex items-center gap-1"
|
||||
>
|
||||
🛢️ Dynamic Data
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
class="block text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-1"
|
||||
>Überschrift (Title)</label
|
||||
>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
id="field-title"
|
||||
name="title"
|
||||
class="w-full bg-[#262626] border border-[#333] focus:border-[#e0436b] rounded-lg p-2.5 text-xs text-white outline-none transition-colors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
class="block text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-1"
|
||||
>Untertitel (Subtitle)</label
|
||||
>
|
||||
<textarea
|
||||
id="field-subtitle"
|
||||
name="subtitle"
|
||||
rows="3"
|
||||
class="w-full bg-[#262626] border border-[#333] focus:border-[#e0436b] rounded-lg p-2.5 text-xs text-white outline-none transition-colors resize-none"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
class="block text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-1"
|
||||
>Button Text / CTA</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="field-cta-text"
|
||||
name="cta_text"
|
||||
class="w-full bg-[#262626] border border-[#333] focus:border-[#e0436b] rounded-lg p-2.5 text-xs text-white outline-none transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
class="block text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-1"
|
||||
>Bild-URL / Medien</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="field-image-url"
|
||||
name="image_url"
|
||||
placeholder="https://..."
|
||||
class="w-full bg-[#262626] border border-[#333] focus:border-[#e0436b] rounded-lg p-2.5 text-xs text-white outline-none transition-colors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB 2: STIL -->
|
||||
<div id="tab-panel-style" class="hidden space-y-5">
|
||||
<div class="pb-2 border-b border-[#2d2d2d]">
|
||||
<span class="text-xs font-bold text-slate-300"
|
||||
>Farben & Effekte</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
class="block text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-1"
|
||||
>Hintergrundfarbe (bg_color)</label
|
||||
>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="color"
|
||||
id="field-bg-color-picker"
|
||||
class="h-9 w-12 rounded bg-[#262626] border border-[#333] cursor-pointer p-0.5"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
id="field-bg-color"
|
||||
name="bg_color"
|
||||
placeholder="#0b0f19"
|
||||
class="flex-1 bg-[#262626] border border-[#333] focus:border-[#e0436b] rounded-lg p-2 text-xs text-white outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
class="block text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-1"
|
||||
>Akzentfarbe (accent_color)</label
|
||||
>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="color"
|
||||
id="field-accent-color-picker"
|
||||
class="h-9 w-12 rounded bg-[#262626] border border-[#333] cursor-pointer p-0.5"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
id="field-accent-color"
|
||||
name="accent_color"
|
||||
placeholder="#38bdf8"
|
||||
class="flex-1 bg-[#262626] border border-[#333] focus:border-[#e0436b] rounded-lg p-2 text-xs text-white outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<label
|
||||
class="text-[11px] font-bold text-slate-400 uppercase tracking-wider"
|
||||
>Backdrop Blur</label
|
||||
>
|
||||
<span id="blur-val" class="text-xs font-mono text-[#38bdf8]"
|
||||
>12px</span
|
||||
>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
id="field-backdrop-blur"
|
||||
name="backdrop_blur"
|
||||
min="0"
|
||||
max="24"
|
||||
value="12"
|
||||
class="w-full accent-[#e0436b]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center justify-between p-3 bg-[#262626] rounded-xl border border-[#333]"
|
||||
>
|
||||
<span class="text-xs font-semibold text-slate-300"
|
||||
>1px Liquid Edge Border</span
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="field-liquid-edge"
|
||||
name="liquid_edge"
|
||||
class="w-4 h-4 accent-[#e0436b] cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB 3: ERWEITERT -->
|
||||
<div id="tab-panel-advanced" class="hidden space-y-5">
|
||||
<div class="pb-2 border-b border-[#2d2d2d]">
|
||||
<span class="text-xs font-bold text-slate-300"
|
||||
>Abstände & Animationen</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
class="block text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-2"
|
||||
>Innenabstände (Padding)</label
|
||||
>
|
||||
<div class="grid grid-cols-4 gap-2 text-center">
|
||||
<div>
|
||||
<span class="text-[10px] text-slate-500 block mb-1"
|
||||
>Oben</span
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="field-padding-top"
|
||||
name="padding_top"
|
||||
placeholder="24px"
|
||||
class="w-full bg-[#262626] border border-[#333] rounded-lg p-1.5 text-xs text-white text-center outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-[10px] text-slate-500 block mb-1"
|
||||
>Rechts</span
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="field-padding-right"
|
||||
name="padding_right"
|
||||
placeholder="16px"
|
||||
class="w-full bg-[#262626] border border-[#333] rounded-lg p-1.5 text-xs text-white text-center outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-[10px] text-slate-500 block mb-1"
|
||||
>Unten</span
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="field-padding-bottom"
|
||||
name="padding_bottom"
|
||||
placeholder="24px"
|
||||
class="w-full bg-[#262626] border border-[#333] rounded-lg p-1.5 text-xs text-white text-center outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-[10px] text-slate-500 block mb-1"
|
||||
>Links</span
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="field-padding-left"
|
||||
name="padding_left"
|
||||
placeholder="16px"
|
||||
class="w-full bg-[#262626] border border-[#333] rounded-lg p-1.5 text-xs text-white text-center outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
class="block text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-1"
|
||||
>Motion.dev Animation</label
|
||||
>
|
||||
<select
|
||||
id="field-animation"
|
||||
name="animation"
|
||||
class="w-full bg-[#262626] border border-[#333] focus:border-[#e0436b] rounded-lg p-2.5 text-xs text-white outline-none"
|
||||
>
|
||||
<option value="">Keine Animation</option>
|
||||
<option value="stagger-fade"
|
||||
>stagger-fade (Schrittweise)</option
|
||||
>
|
||||
<option value="spring-scale"
|
||||
>spring-scale (Federnder Zoom)</option
|
||||
>
|
||||
<option value="hover-pulse"
|
||||
>hover-pulse (Interaktiv Puls)</option
|
||||
>
|
||||
<option value="slide-up">slide-up (Von unten)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 pt-2">
|
||||
<label
|
||||
class="block text-[11px] font-bold text-slate-400 uppercase tracking-wider"
|
||||
>Sichtbarkeit (Responsive)</label
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between p-2.5 bg-[#262626] rounded-lg border border-[#333]"
|
||||
>
|
||||
<span class="text-xs text-slate-300"
|
||||
>Auf Mobile ausblenden</span
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="field-hide-mobile"
|
||||
name="hide_mobile"
|
||||
class="accent-[#e0436b]"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center justify-between p-2.5 bg-[#262626] rounded-lg border border-[#333]"
|
||||
>
|
||||
<span class="text-xs text-slate-300"
|
||||
>Auf Desktop ausblenden</span
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="field-hide-desktop"
|
||||
name="hide_desktop"
|
||||
class="accent-[#e0436b]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. BOTTOM-BAR (PRO-STEUERUNGSLEISTE) -->
|
||||
<div
|
||||
class="h-14 bg-[#181818] border-t border-[#2d2d2d] px-3 flex items-center justify-between gap-2"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<!-- ⚙️ Zahnrad: Seiteneinstellungen -->
|
||||
<button
|
||||
id="btn-open-settings"
|
||||
class="p-2 rounded hover:bg-[#2a2a2a] text-slate-400 hover:text-white transition-colors"
|
||||
title="Seiteneinstellungen (SEO, Slug)"
|
||||
>
|
||||
⚙️
|
||||
</button>
|
||||
|
||||
<!-- 👁️ Auge: Vorschau im neuen Tab -->
|
||||
<a
|
||||
href={targetPage.slug || "/"}
|
||||
target="_blank"
|
||||
class="p-2 rounded hover:bg-[#2a2a2a] text-slate-400 hover:text-white transition-colors"
|
||||
title="Live Vorschau im Tab"
|
||||
>
|
||||
👁️
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 📱 Responsive-Switcher -->
|
||||
<div
|
||||
class="flex items-center bg-[#262626] rounded-lg p-1 border border-[#333]"
|
||||
>
|
||||
<button
|
||||
id="res-desktop"
|
||||
class="p-1 rounded text-[#38bdf8] hover:bg-[#333] transition-colors"
|
||||
title="Desktop View (100%)">🖥️</button
|
||||
>
|
||||
<button
|
||||
id="res-tablet"
|
||||
class="p-1 rounded text-slate-400 hover:text-white hover:bg-[#333] transition-colors"
|
||||
title="Tablet View (768px)">💻</button
|
||||
>
|
||||
<button
|
||||
id="res-mobile"
|
||||
class="p-1 rounded text-slate-400 hover:text-white hover:bg-[#333] transition-colors"
|
||||
title="Mobile View (375px)">📱</button
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- SPEICHERN & VERÖFFENTLICHEN -->
|
||||
<div class="flex items-center gap-1.5">
|
||||
<button
|
||||
id="btn-save-draft"
|
||||
class="px-2.5 py-1.5 bg-[#262626] hover:bg-[#333] text-slate-200 font-bold rounded-lg text-xs transition-colors border border-[#333]"
|
||||
title="Entwurf speichern"
|
||||
>
|
||||
💾
|
||||
</button>
|
||||
<button
|
||||
id="btn-publish"
|
||||
class="px-3 py-1.5 bg-[#e0436b] hover:bg-[#c9365c] text-white font-bold rounded-lg text-xs transition-colors shadow-lg shadow-[#e0436b]/20 flex items-center gap-1"
|
||||
>
|
||||
🚀 Veröffentlichen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- RECHTER CANVAS-BEREICH (LIVE-IFRAME WORKSPACE) -->
|
||||
<main
|
||||
class="flex-1 bg-[#121212] flex flex-col relative items-center justify-start overflow-hidden p-4"
|
||||
>
|
||||
<div
|
||||
class="w-full h-8 bg-[#181818] border border-[#2d2d2d] rounded-t-xl flex items-center justify-between px-4 text-xs text-slate-400 mb-0"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-2.5 h-2.5 rounded-full bg-emerald-500"></span>
|
||||
<span
|
||||
>Elementor Canvas: <code class="text-[#38bdf8] font-mono"
|
||||
>{previewUrl}</code
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
<span
|
||||
id="editor-status-badge"
|
||||
class="text-[#38bdf8] bg-[#38bdf8]/10 border border-[#38bdf8]/30 px-2 py-0.5 rounded text-[10px] font-mono"
|
||||
>
|
||||
Ready
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Live Canvas Iframe Wrapper -->
|
||||
<div
|
||||
id="iframe-wrapper"
|
||||
class="w-full h-[calc(100%-2rem)] transition-all duration-300 flex justify-center items-center shadow-2xl rounded-b-xl overflow-hidden bg-slate-950 border border-[#2d2d2d]"
|
||||
>
|
||||
<iframe
|
||||
id="preview-iframe"
|
||||
src={previewUrl}
|
||||
class="w-full h-full border-none transition-all duration-300"
|
||||
title="Elementor Live Workspace"></iframe>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- GLOBALE SEITENEINSTELLUNGEN MODAL -->
|
||||
<dialog
|
||||
id="modal-page-settings"
|
||||
class="bg-[#1e1e1e] text-white border border-[#333] rounded-2xl p-6 backdrop:bg-black/80 max-w-md w-full shadow-2xl"
|
||||
>
|
||||
<form id="form-page-settings" class="space-y-4">
|
||||
<h2
|
||||
class="text-base font-bold text-white flex items-center justify-between border-b border-[#2d2d2d] pb-3"
|
||||
>
|
||||
<span>⚙️ Seiteneinstellungen</span>
|
||||
</h2>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-400 mb-1"
|
||||
>Seitentitel</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
name="title"
|
||||
value={targetPage.title}
|
||||
class="w-full bg-[#262626] border border-[#333] rounded-lg p-2 text-xs text-white focus:outline-none focus:border-[#e0436b]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-400 mb-1"
|
||||
>Slug (URL)</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
name="slug"
|
||||
value={targetPage.slug}
|
||||
class="w-full bg-[#262626] border border-[#333] rounded-lg p-2 text-xs text-white focus:outline-none focus:border-[#e0436b]"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2 pt-4 border-t border-[#2d2d2d]">
|
||||
<button
|
||||
type="button"
|
||||
id="btn-close-page-settings"
|
||||
class="px-4 py-2 bg-[#262626] text-slate-300 rounded-lg text-xs hover:bg-[#333]"
|
||||
>Abbrechen</button
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
class="px-4 py-2 bg-[#e0436b] text-white font-bold rounded-lg text-xs hover:bg-[#c9365c]"
|
||||
>Speichern</button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: #181818;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: #333;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: #444;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const iframe = document.getElementById("preview-iframe") as HTMLIFrameElement;
|
||||
const wrapper = document.getElementById("iframe-wrapper");
|
||||
const statusBadge = document.getElementById("editor-status-badge");
|
||||
|
||||
const stateWidgets = document.getElementById("panel-state-widgets");
|
||||
const stateInspector = document.getElementById("panel-state-inspector");
|
||||
const activeBadge = document.getElementById("inspector-active-badge");
|
||||
|
||||
// 1. Live PostMessage Sync Helper
|
||||
function sendToIframe(type: string, payload: any) {
|
||||
if (iframe?.contentWindow) {
|
||||
iframe.contentWindow.postMessage({ type, payload }, "*");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Responsive Switcher Controls
|
||||
document.getElementById("res-desktop")?.addEventListener("click", () => {
|
||||
if (wrapper) wrapper.style.width = "100%";
|
||||
});
|
||||
document.getElementById("res-tablet")?.addEventListener("click", () => {
|
||||
if (wrapper) wrapper.style.width = "768px";
|
||||
});
|
||||
document.getElementById("res-mobile")?.addEventListener("click", () => {
|
||||
if (wrapper) wrapper.style.width = "375px";
|
||||
});
|
||||
|
||||
// 3. Tab Switching in Inspector
|
||||
const btnContent = document.getElementById("tab-btn-content");
|
||||
const btnStyle = document.getElementById("tab-btn-style");
|
||||
const btnAdv = document.getElementById("tab-btn-advanced");
|
||||
|
||||
const panelContent = document.getElementById("tab-panel-content");
|
||||
const panelStyle = document.getElementById("tab-panel-style");
|
||||
const panelAdv = document.getElementById("tab-panel-advanced");
|
||||
|
||||
function switchTab(tab: "content" | "style" | "advanced") {
|
||||
[btnContent, btnStyle, btnAdv].forEach((b) => {
|
||||
b?.classList.remove("text-[#e0436b]", "border-[#e0436b]");
|
||||
b?.classList.add("text-slate-400", "border-transparent");
|
||||
});
|
||||
[panelContent, panelStyle, panelAdv].forEach((p) =>
|
||||
p?.classList.add("hidden"),
|
||||
);
|
||||
|
||||
if (tab === "content") {
|
||||
btnContent?.classList.add("text-[#e0436b]", "border-[#e0436b]");
|
||||
panelContent?.classList.remove("hidden");
|
||||
} else if (tab === "style") {
|
||||
btnStyle?.classList.add("text-[#e0436b]", "border-[#e0436b]");
|
||||
panelStyle?.classList.remove("hidden");
|
||||
} else if (tab === "advanced") {
|
||||
btnAdv?.classList.add("text-[#e0436b]", "border-[#e0436b]");
|
||||
panelAdv?.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
btnContent?.addEventListener("click", () => switchTab("content"));
|
||||
btnStyle?.addEventListener("click", () => switchTab("style"));
|
||||
btnAdv?.addEventListener("click", () => switchTab("advanced"));
|
||||
|
||||
// Open Drawer button
|
||||
document.getElementById("btn-open-widgets")?.addEventListener("click", () => {
|
||||
stateInspector?.classList.add("hidden");
|
||||
stateWidgets?.classList.remove("hidden");
|
||||
activeBadge?.classList.add("hidden");
|
||||
});
|
||||
|
||||
// Color Pickers Sync
|
||||
const bgPicker = document.getElementById(
|
||||
"field-bg-color-picker",
|
||||
) as HTMLInputElement;
|
||||
const bgInput = document.getElementById("field-bg-color") as HTMLInputElement;
|
||||
bgPicker?.addEventListener("input", (e: any) => {
|
||||
if (bgInput) bgInput.value = e.target.value;
|
||||
});
|
||||
|
||||
const accPicker = document.getElementById(
|
||||
"field-accent-color-picker",
|
||||
) as HTMLInputElement;
|
||||
const accInput = document.getElementById(
|
||||
"field-accent-color",
|
||||
) as HTMLInputElement;
|
||||
accPicker?.addEventListener("input", (e: any) => {
|
||||
if (accInput) accInput.value = e.target.value;
|
||||
});
|
||||
|
||||
const blurSlider = document.getElementById(
|
||||
"field-backdrop-blur",
|
||||
) as HTMLInputElement;
|
||||
const blurVal = document.getElementById("blur-val");
|
||||
blurSlider?.addEventListener("input", (e: any) => {
|
||||
if (blurVal) blurVal.textContent = `${e.target.value}px`;
|
||||
});
|
||||
|
||||
// Inspector für Sektion öffnen
|
||||
document.querySelectorAll(".btn-inspect-section").forEach((btn) => {
|
||||
btn.addEventListener("click", (e: any) => {
|
||||
const card = e.target.closest("[data-section-id]");
|
||||
const sectionId = card.getAttribute("data-section-id");
|
||||
const settings = JSON.parse(card.getAttribute("data-settings") || "{}");
|
||||
|
||||
(document.getElementById("active-section-id") as HTMLInputElement).value =
|
||||
sectionId;
|
||||
(document.getElementById("field-title") as HTMLInputElement).value =
|
||||
settings.title || "";
|
||||
(document.getElementById("field-subtitle") as HTMLTextAreaElement).value =
|
||||
settings.subtitle || "";
|
||||
(document.getElementById("field-cta-text") as HTMLInputElement).value =
|
||||
settings.cta_text || "";
|
||||
(document.getElementById("field-bg-color") as HTMLInputElement).value =
|
||||
settings.bg_color || "";
|
||||
(
|
||||
document.getElementById("field-accent-color") as HTMLInputElement
|
||||
).value = settings.accent_color || "";
|
||||
|
||||
stateWidgets?.classList.add("hidden");
|
||||
stateInspector?.classList.remove("hidden");
|
||||
activeBadge?.classList.remove("hidden");
|
||||
switchTab("content");
|
||||
});
|
||||
});
|
||||
|
||||
// Dynamic Live Patch Event bei Eingaben
|
||||
const formInspector = document.getElementById("form-elementor-inspector");
|
||||
let autoSaveTimeout: any = null;
|
||||
|
||||
formInspector?.addEventListener("input", () => {
|
||||
const formData = new FormData(formInspector as HTMLFormElement);
|
||||
const sectionId = formData.get("section_id") as string;
|
||||
const settings: Record<string, any> = {};
|
||||
formData.forEach((val, key) => {
|
||||
if (key !== "section_id") settings[key] = val;
|
||||
});
|
||||
|
||||
// 1. Sofortiges PostMessage Live Rendering im Canvas
|
||||
sendToIframe("THEME_UPDATE", { sectionId, settings });
|
||||
|
||||
// 2. Debounced API Persistenz in SQLite DB
|
||||
if (autoSaveTimeout) clearTimeout(autoSaveTimeout);
|
||||
autoSaveTimeout = setTimeout(async () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const pageId = urlParams.get("page_id") || "page_home";
|
||||
await fetch("/api/editor/update-section-settings", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
page_id: pageId,
|
||||
section_id: sectionId,
|
||||
settings,
|
||||
}),
|
||||
});
|
||||
if (statusBadge) {
|
||||
statusBadge.textContent = "Live Synced";
|
||||
statusBadge.className =
|
||||
"text-emerald-400 bg-emerald-950/60 border border-emerald-800/60 px-2 py-0.5 rounded text-[10px] font-mono";
|
||||
}
|
||||
}, 400);
|
||||
});
|
||||
|
||||
// 4. API Actions (Save Draft & Publish)
|
||||
document
|
||||
.getElementById("btn-save-draft")
|
||||
?.addEventListener("click", async () => {
|
||||
const res = await fetch("/api/editor/save-draft", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ updated_at: new Date().toISOString() }),
|
||||
});
|
||||
if (res.ok && statusBadge) {
|
||||
statusBadge.textContent = "Draft Saved";
|
||||
statusBadge.className =
|
||||
"text-emerald-400 bg-emerald-950/60 border border-emerald-800/60 px-2 py-0.5 rounded text-[10px] font-mono";
|
||||
}
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("btn-publish")
|
||||
?.addEventListener("click", async () => {
|
||||
const res = await fetch("/api/editor/publish", { method: "POST" });
|
||||
if (res.ok && statusBadge) {
|
||||
statusBadge.textContent = "Published!";
|
||||
statusBadge.className =
|
||||
"text-emerald-400 bg-emerald-950/60 border border-emerald-800/60 px-2 py-0.5 rounded text-[10px] font-mono";
|
||||
sendToIframe("RELOAD_CANVAS", {});
|
||||
}
|
||||
});
|
||||
|
||||
// Move Up / Move Down / Delete Handlers
|
||||
function getSectionsFromDOM() {
|
||||
const list: any[] = [];
|
||||
document.querySelectorAll('#section-list > div').forEach(card => {
|
||||
const id = card.getAttribute('data-section-id');
|
||||
const type = card.getAttribute('data-section-type');
|
||||
const settings = JSON.parse(card.getAttribute('data-settings') || '{}');
|
||||
list.push({ id, type, settings });
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
async function updateSectionsOrder(newSections: any[]) {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const pageId = urlParams.get('page_id') || 'page_home';
|
||||
const res = await fetch('/api/editor/update-section', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ page_id: pageId, sections: newSections })
|
||||
});
|
||||
if (res.ok) {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll('.btn-move-up').forEach(btn => {
|
||||
btn.addEventListener('click', async (e: any) => {
|
||||
e.stopPropagation();
|
||||
const sections = getSectionsFromDOM();
|
||||
const card = e.target.closest('[data-section-id]');
|
||||
const sectionId = card.getAttribute('data-section-id');
|
||||
const index = sections.findIndex(s => s.id === sectionId);
|
||||
if (index > 0) {
|
||||
const temp = sections[index];
|
||||
sections[index] = sections[index - 1];
|
||||
sections[index - 1] = temp;
|
||||
await updateSectionsOrder(sections);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.btn-move-down').forEach(btn => {
|
||||
btn.addEventListener('click', async (e: any) => {
|
||||
e.stopPropagation();
|
||||
const sections = getSectionsFromDOM();
|
||||
const card = e.target.closest('[data-section-id]');
|
||||
const sectionId = card.getAttribute('data-section-id');
|
||||
const index = sections.findIndex(s => s.id === sectionId);
|
||||
if (index >= 0 && index < sections.length - 1) {
|
||||
const temp = sections[index];
|
||||
sections[index] = sections[index + 1];
|
||||
sections[index + 1] = temp;
|
||||
await updateSectionsOrder(sections);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.btn-delete-section').forEach(btn => {
|
||||
btn.addEventListener('click', async (e: any) => {
|
||||
e.stopPropagation();
|
||||
const card = e.target.closest('[data-section-id]');
|
||||
const sectionId = card?.getAttribute('data-section-id');
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const pageId = urlParams.get('page_id') || 'page_home';
|
||||
|
||||
const res = await fetch('/api/editor/delete-section', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ page_id: pageId, section_id: sectionId })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Quick Add Widget Button
|
||||
document.querySelectorAll(".btn-quick-add-widget").forEach((btn) => {
|
||||
btn.addEventListener("click", async (e: any) => {
|
||||
const type = e.currentTarget.getAttribute("data-widget-type");
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const pageId = urlParams.get("page_id") || "page_home";
|
||||
|
||||
const res = await fetch("/api/editor/add-section", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ page_id: pageId, section_type: type }),
|
||||
});
|
||||
if (res.ok) window.location.reload();
|
||||
});
|
||||
});
|
||||
|
||||
// Page Switcher Dropdown Handler
|
||||
document.getElementById("editor-page-switcher")?.addEventListener("change", (e: any) => {
|
||||
const selectedPageId = e.target.value;
|
||||
window.location.href = `/admin/editor/?page_id=${selectedPageId}`;
|
||||
});
|
||||
|
||||
// Page Settings Modal
|
||||
const modalPage = document.getElementById(
|
||||
"modal-page-settings",
|
||||
) as HTMLDialogElement;
|
||||
document
|
||||
.getElementById("btn-open-settings")
|
||||
?.addEventListener("click", () => modalPage?.showModal());
|
||||
document
|
||||
.getElementById("btn-close-page-settings")
|
||||
?.addEventListener("click", () => modalPage?.close());
|
||||
</script>
|
||||
333
src/pages/admin/index.astro
Normal file
333
src/pages/admin/index.astro
Normal file
@@ -0,0 +1,333 @@
|
||||
---
|
||||
import Layout from '../../layouts/Layout.astro';
|
||||
import AdminSidebar from '../../components/admin/AdminSidebar.astro';
|
||||
import { loadWaasConfigs } from '../../utils/configLoader';
|
||||
import { THEME_PRESETS } from '../../utils/themePresets';
|
||||
|
||||
const { siteConfig, themeConfig, smtpConfig } = loadWaasConfigs(Astro.request.headers.get('user-agent') ?? '');
|
||||
const presets = Object.values(THEME_PRESETS);
|
||||
---
|
||||
|
||||
<Layout title="Waas Admin Dashboard | Layout and Config Management">
|
||||
<AdminSidebar currentPath="/admin" />
|
||||
<div class="min-h-screen bg-[#0B0F19] text-white p-6 sm:p-10 pl-16 sm:pl-20 font-sans">
|
||||
|
||||
<div class="max-w-6xl mx-auto space-y-10">
|
||||
|
||||
<!-- Admin Header & Quick Navigation -->
|
||||
<div class="space-y-4 border-b border-white/10 pb-6">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<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">System & Admin Dashboard</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<a id="toggle-preview-mode" href="/?preview=draft" target="_blank" class="px-4 py-2 rounded-xl bg-amber-500/10 text-amber-400 border border-amber-500/20 text-xs font-semibold hover:bg-amber-500 hover:text-slate-950 transition-all flex items-center gap-2">
|
||||
🛠️ Bearbeitungsmodus (Entwurf-Vorschau)
|
||||
</a>
|
||||
<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-950 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>
|
||||
|
||||
<!-- Quick Admin Nav Cards / Links -->
|
||||
<nav aria-label="Admin Navigation" class="pt-2">
|
||||
<a href="/admin/pages" class="p-4 bg-slate-900/80 border border-white/10 hover:border-sky-400/50 rounded-xl flex items-center justify-between text-xs transition-all group">
|
||||
<div>
|
||||
<span class="font-bold text-white block group-hover:text-sky-400 text-sm">📄 Seiten-Verwaltung & Visueller Editor</span>
|
||||
<span class="text-[11px] text-slate-400">Routen, Slugs, Startseite & Seiten im Editor öffnen</span>
|
||||
</div>
|
||||
<span class="text-sky-400 text-base font-bold">→</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
|
||||
</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 & Stammdaten -->
|
||||
<div class="glass-panel rounded-2xl p-6 space-y-6">
|
||||
<h2 class="text-lg font-bold text-sky-400 uppercase tracking-wider flex items-center gap-2">
|
||||
Allgemeine Informationen & SEO
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-400 mb-1">Webseiten-Name (Titel)</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 (SEO)</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>
|
||||
<label class="flex items-center gap-3 pt-2 p-3 bg-amber-500/10 border border-amber-500/20 rounded-xl cursor-pointer">
|
||||
<input type="checkbox" id="isDemoMode" checked={!!siteConfig.is_demo_mode} class="rounded text-amber-400 focus:ring-0 h-4 w-4" />
|
||||
<div>
|
||||
<span class="text-xs text-amber-400 font-bold block">🎭 Demo-Modus aktivieren</span>
|
||||
<span class="text-[10px] text-slate-400">Zeigt einen festen Demo-Hinweis für Interessenten auf der Live-Website</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
|
||||
<h3 class="text-xs font-bold text-slate-400 uppercase tracking-wider pt-2 border-t border-white/10">Unternehmens-Stammdaten (Dynamic Data Binding)</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-400 mb-1">Firmenname / Brand</label>
|
||||
<input type="text" id="infoTitle" value={(siteConfig as any).site_info?.title || 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-semibold text-slate-400 mb-1">E-Mail Adresse</label>
|
||||
<input type="email" id="infoEmail" value={(siteConfig as any).site_info?.email || ''} 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-semibold text-slate-400 mb-1">Telefonnummer</label>
|
||||
<input type="text" id="infoPhone" value={(siteConfig as any).site_info?.phone || ''} 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-semibold text-slate-400 mb-1">Adresse / Standort</label>
|
||||
<input type="text" id="infoAddress" value={(siteConfig as any).site_info?.address || ''} 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>
|
||||
|
||||
<!-- Global Header Config -->
|
||||
<h3 class="text-xs font-bold text-slate-400 uppercase tracking-wider pt-4 border-t border-white/10">Globaler Header / Navigation Bar</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-400 mb-1">Brand Name (z.B. N&D)</label>
|
||||
<input type="text" id="headerBrandName" value={siteConfig.header?.brandName || 'N&D'} 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-semibold text-slate-400 mb-1">Brand Highlight (z.B. i-t SOLUTIONS)</label>
|
||||
<input type="text" id="headerBrandHighlight" value={siteConfig.header?.brandHighlight || 'i-t SOLUTIONS'} 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-semibold text-slate-400 mb-1">CTA Button Text</label>
|
||||
<input type="text" id="headerCtaText" value={siteConfig.header?.ctaText || 'Erstgespräch buchen'} 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-semibold text-slate-400 mb-1">CTA Button Link</label>
|
||||
<input type="text" id="headerCtaLink" value={siteConfig.header?.ctaLink || '#contact'} 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>
|
||||
<label class="flex items-center gap-2 cursor-pointer sm:col-span-2">
|
||||
<input type="checkbox" id="headerShowThemeToggle" checked={siteConfig.header?.showThemeToggle !== false} class="rounded text-sky-400 focus:ring-0" />
|
||||
<span class="text-xs text-slate-300 font-semibold">Theme-Toggle Button (☀️/🌙) anzeigen</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer sm:col-span-2">
|
||||
<input type="checkbox" id="headerGlassEffect" checked={siteConfig.header?.glassEffect !== false} class="rounded text-sky-400 focus:ring-0" />
|
||||
<span class="text-xs text-slate-300 font-semibold">Liquid Glassmorphism Effekt aktivieren</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Global Footer Config -->
|
||||
<h3 class="text-xs font-bold text-slate-400 uppercase tracking-wider pt-4 border-t border-white/10">Globaler Footer</h3>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-400 mb-1">Footer Beschreibungstext</label>
|
||||
<textarea id="footerBrandDescription" 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.footer?.brandDescription || ''}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-400 mb-1">Copyright Text</label>
|
||||
<input type="text" id="footerCopyrightText" value={siteConfig.footer?.copyrightText || ''} 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>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" id="footerShowLegalLinks" checked={siteConfig.footer?.showLegalLinks !== false} class="rounded text-sky-400 focus:ring-0" />
|
||||
<span class="text-xs text-slate-300 font-semibold">Rechtliche Links (Datenschutz & Impressum) im Footer anzeigen</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Sidebar Column -->
|
||||
<div class="space-y-8">
|
||||
|
||||
<!-- Farb-Auswahl (10 globale Farb-Kombinationen) & Custom Colors -->
|
||||
<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">
|
||||
🎨 Farb-Auswahl (10 Farb-Kombinationen)
|
||||
</h2>
|
||||
<p class="text-[10px] text-slate-400">Wähle eine globale Farbkombination für das gesamte Farbspektrum:</p>
|
||||
<div class="space-y-2 max-h-[260px] 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-2.5 h-2.5 rounded-full inline-block border border-white/20" style={`background-color: ${p.colors.bg};`} title="Hintergrund"></span>
|
||||
<span class="w-2.5 h-2.5 rounded-full inline-block border border-white/20" style={`background-color: ${p.colors.cardBg};`} title="Karten"></span>
|
||||
<span class="w-2.5 h-2.5 rounded-full inline-block border border-white/20" style={`background-color: ${p.colors.text};`} title="Text"></span>
|
||||
<span class="w-2.5 h-2.5 rounded-full inline-block border border-white/20" style={`background-color: ${p.colors.accent};`} title="Akzent"></span>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Custom Colors Section -->
|
||||
<h3 class="text-xs font-bold text-slate-400 uppercase tracking-wider pt-3 border-t border-white/10">Individuelle Farbvergabe (Overrides)</h3>
|
||||
<p class="text-[10px] text-slate-400">Überschreibe hier gezielt einzelne Preset-Farben:</p>
|
||||
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||
<div>
|
||||
<label class="block text-[10px] text-slate-400 mb-1">Hintergrund (Bg)</label>
|
||||
<input type="color" id="customColorBg" value={themeConfig.customColors?.bg || (THEME_PRESETS[themeConfig.activePreset] || THEME_PRESETS.corporateDark).colors.bg} class="w-full h-8 rounded bg-slate-900 border border-white/10 cursor-pointer p-0.5" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] text-slate-400 mb-1">Akzentfarbe</label>
|
||||
<input type="color" id="customColorAccent" value={themeConfig.customColors?.accent || (THEME_PRESETS[themeConfig.activePreset] || THEME_PRESETS.corporateDark).colors.accent} class="w-full h-8 rounded bg-slate-900 border border-white/10 cursor-pointer p-0.5" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] text-slate-400 mb-1">Textfarbe</label>
|
||||
<input type="color" id="customColorText" value={themeConfig.customColors?.text || (THEME_PRESETS[themeConfig.activePreset] || THEME_PRESETS.corporateDark).colors.text} class="w-full h-8 rounded bg-slate-900 border border-white/10 cursor-pointer p-0.5" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] text-slate-400 mb-1">Rahmenfarbe (Border)</label>
|
||||
<input type="text" id="customColorBorder" value={themeConfig.customColors?.border || ''} placeholder="rgba(255,255,255,0.1)" class="w-full h-8 rounded bg-slate-900 border border-white/10 text-[10px] px-2 text-white" />
|
||||
</div>
|
||||
</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 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'),
|
||||
is_demo_mode: getBool('isDemoMode'),
|
||||
site_info: {
|
||||
title: getVal('infoTitle'),
|
||||
email: getVal('infoEmail'),
|
||||
phone: getVal('infoPhone'),
|
||||
address: getVal('infoAddress')
|
||||
},
|
||||
header: {
|
||||
brandName: getVal('headerBrandName'),
|
||||
brandHighlight: getVal('headerBrandHighlight'),
|
||||
ctaText: getVal('headerCtaText'),
|
||||
ctaLink: getVal('headerCtaLink'),
|
||||
showThemeToggle: getBool('headerShowThemeToggle'),
|
||||
glassEffect: getBool('headerGlassEffect')
|
||||
},
|
||||
footer: {
|
||||
brandDescription: getVal('footerBrandDescription'),
|
||||
copyrightText: getVal('footerCopyrightText'),
|
||||
showLegalLinks: getBool('footerShowLegalLinks')
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
themeConfig: {
|
||||
activePreset: selectedPreset,
|
||||
customColors: {
|
||||
bg: getVal('customColorBg'),
|
||||
accent: getVal('customColorAccent'),
|
||||
text: getVal('customColorText'),
|
||||
border: getVal('customColorBorder')
|
||||
}
|
||||
},
|
||||
|
||||
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>
|
||||
188
src/pages/admin/pages.astro
Normal file
188
src/pages/admin/pages.astro
Normal file
@@ -0,0 +1,188 @@
|
||||
---
|
||||
// src/pages/admin/pages.astro
|
||||
import Layout from '../../layouts/Layout.astro';
|
||||
import AdminSidebar from '../../components/admin/AdminSidebar.astro';
|
||||
import { loadWaasConfigs } from '../../utils/configLoader';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
const userAgent = Astro.request.headers.get('user-agent') ?? '';
|
||||
const { siteConfig } = loadWaasConfigs(userAgent);
|
||||
const pages = (siteConfig as any).pages || [];
|
||||
const currentHomepageId = (siteConfig as any).site_info?.homepage_id;
|
||||
---
|
||||
|
||||
<Layout title="Seiten-Verwaltung | Admin Dashboard">
|
||||
<AdminSidebar currentPath="/admin/pages" />
|
||||
<div class="max-w-6xl mx-auto px-4 py-8 pl-16 sm:pl-20">
|
||||
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-white">Seiten-Verwaltung</h1>
|
||||
<p class="text-slate-400 text-sm">Verwalte hier deine Routen, Slugs und lege die Startseite fest.</p>
|
||||
</div>
|
||||
<button
|
||||
id="btn-open-modal"
|
||||
class="bg-sky-500 hover:bg-sky-400 text-slate-950 font-bold px-4 py-2 rounded-lg text-sm transition-colors"
|
||||
>
|
||||
+ Neue Seite anlegen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden shadow-xl">
|
||||
<table class="w-full text-left text-sm text-slate-300">
|
||||
<thead class="bg-slate-950/60 text-slate-400 border-b border-slate-800 uppercase text-xs">
|
||||
<tr>
|
||||
<th class="p-4">Startseite</th>
|
||||
<th class="p-4">Titel</th>
|
||||
<th class="p-4">URL Slug</th>
|
||||
<th class="p-4">Status</th>
|
||||
<th class="p-4 text-right">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-800/60">
|
||||
{pages.map((page: any) => {
|
||||
const isHomepage = page.id === currentHomepageId;
|
||||
return (
|
||||
<tr class="hover:bg-slate-800/40 transition-colors">
|
||||
<td class="p-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="homepage"
|
||||
checked={isHomepage}
|
||||
data-page-id={page.id}
|
||||
class="btn-set-homepage cursor-pointer accent-sky-400 h-4 w-4"
|
||||
title="Als Startseite festlegen"
|
||||
/>
|
||||
</td>
|
||||
<td class="p-4 font-semibold text-white">
|
||||
{page.title}
|
||||
{isHomepage && <span class="ml-2 text-xs text-sky-400 bg-sky-950 border border-sky-800 px-2 py-0.5 rounded-full">Homepage</span>}
|
||||
</td>
|
||||
<td class="p-4 font-mono text-xs text-slate-400">{page.slug}</td>
|
||||
<td class="p-4">
|
||||
{page.is_published ? (
|
||||
<span class="text-emerald-400 bg-emerald-950/60 border border-emerald-800 px-2 py-0.5 rounded text-xs">Live</span>
|
||||
) : (
|
||||
<span class="text-amber-400 bg-amber-950/60 border border-amber-800 px-2 py-0.5 rounded text-xs">Draft</span>
|
||||
)}
|
||||
</td>
|
||||
<td class="p-4 text-right space-x-2">
|
||||
<a
|
||||
href={page.slug}
|
||||
target="_blank"
|
||||
class="bg-slate-800 hover:bg-slate-700 text-sky-400 px-2.5 py-1.5 rounded text-xs font-medium inline-block transition-colors"
|
||||
title="Seite im Browser öffnen"
|
||||
>
|
||||
🔗 Öffnen
|
||||
</a>
|
||||
<a
|
||||
href={`/admin/editor/?page_id=${page.id}`}
|
||||
class="bg-sky-500/20 hover:bg-sky-500 text-sky-300 hover:text-slate-950 px-2.5 py-1.5 rounded text-xs font-medium inline-block transition-colors border border-sky-500/30"
|
||||
>
|
||||
✏️ Editor
|
||||
</a>
|
||||
{!isHomepage && (
|
||||
<button
|
||||
type="button"
|
||||
data-page-id={page.id}
|
||||
data-page-title={page.title}
|
||||
class="btn-delete-page bg-red-500/10 hover:bg-red-500 text-red-400 hover:text-white px-2.5 py-1.5 rounded text-xs font-medium inline-block transition-colors border border-red-500/20"
|
||||
title="Seite löschen"
|
||||
>
|
||||
🗑️ Löschen
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dialog id="modal-add-page" class="bg-slate-900 text-white border border-slate-800 rounded-xl p-6 backdrop:bg-slate-950/80 max-w-md w-full">
|
||||
<form id="form-create-page" class="space-y-4">
|
||||
<h2 class="text-lg font-bold text-white">Neue Seite anlegen</h2>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs text-slate-400 mb-1">Seitentitel</label>
|
||||
<input type="text" name="title" required placeholder="z. B. Unsere Leistungen" class="w-full bg-slate-800 border border-slate-700 rounded p-2 text-sm text-white focus:outline-none focus:border-sky-500" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs text-slate-400 mb-1">URL Slug</label>
|
||||
<input type="text" name="slug" required placeholder="z. B. leistungen" class="w-full bg-slate-800 border border-slate-700 rounded p-2 text-sm text-white focus:outline-none focus:border-sky-500" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 pt-4">
|
||||
<button type="button" id="btn-close-modal" class="px-4 py-2 bg-slate-800 text-slate-300 rounded text-xs">Abbrechen</button>
|
||||
<button type="submit" class="px-4 py-2 bg-sky-500 text-slate-950 font-bold rounded text-xs">Seite erstellen</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
// Event-Handler für Startseiten-Wechsel & Dialog-Modal
|
||||
const modal = document.getElementById('modal-add-page') as HTMLDialogElement;
|
||||
document.getElementById('btn-open-modal')?.addEventListener('click', () => modal?.showModal());
|
||||
document.getElementById('btn-close-modal')?.addEventListener('click', () => modal?.close());
|
||||
|
||||
// Radio-Button Event: homepage_id ändern
|
||||
document.querySelectorAll('.btn-set-homepage').forEach(radio => {
|
||||
radio.addEventListener('change', async (e: any) => {
|
||||
const pageId = e.target.getAttribute('data-page-id');
|
||||
await fetch('/api/admin/pages/set-homepage', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ page_id: pageId })
|
||||
});
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
|
||||
// Form Event: Neue Seite anlegen
|
||||
document.getElementById('form-create-page')?.addEventListener('submit', async (e: Event) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
const title = formData.get('title');
|
||||
const slug = formData.get('slug');
|
||||
|
||||
const res = await fetch('/api/admin/pages/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title, slug })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
|
||||
// Event: Seite löschen
|
||||
document.querySelectorAll('.btn-delete-page').forEach(btn => {
|
||||
btn.addEventListener('click', async (e: any) => {
|
||||
const pageId = e.target.getAttribute('data-page-id');
|
||||
const pageTitle = e.target.getAttribute('data-page-title');
|
||||
|
||||
if (!confirm(`Möchten Sie die Seite "${pageTitle}" wirklich unwiderruflich löschen?`)) return;
|
||||
|
||||
const res = await fetch('/api/admin/pages/delete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ page_id: pageId })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
alert(data.error || 'Fehler beim Löschen der Seite.');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
78
src/pages/admin/settings.astro
Normal file
78
src/pages/admin/settings.astro
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
// src/pages/admin/settings.astro
|
||||
import Layout from '../../layouts/Layout.astro';
|
||||
import AdminSidebar from '../../components/admin/AdminSidebar.astro';
|
||||
import { loadWaasConfigs } from '../../utils/configLoader';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
const userAgent = Astro.request.headers.get('user-agent') ?? '';
|
||||
const { siteConfig } = loadWaasConfigs(userAgent);
|
||||
const info = (siteConfig as any).site_info || {};
|
||||
---
|
||||
|
||||
<Layout title="Globale Einstellungen | N&D Admin">
|
||||
<AdminSidebar currentPath="/admin/settings" />
|
||||
<div class="max-w-4xl mx-auto px-4 py-8 pl-16 sm:pl-20">
|
||||
|
||||
<div class="flex items-center justify-between mb-8 border-b border-slate-800 pb-4">
|
||||
<div>
|
||||
<a href="/admin/pages" class="text-xs text-slate-400 hover:text-sky-400 transition-colors">← Zurück zum Seiten-Dashboard</a>
|
||||
<h1 class="text-2xl font-bold text-white mt-1">Globale Stammdaten & SEO</h1>
|
||||
<p class="text-slate-400 text-sm">Diese Daten werden für automatisches Data Binding & Kontakt-Infos genutzt.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="form-site-settings" class="space-y-6">
|
||||
<div class="bg-slate-900 border border-slate-800 rounded-xl p-6 space-y-4 shadow-xl">
|
||||
<h2 class="text-base font-bold text-sky-400 border-b border-slate-800 pb-2">Unternehmensdaten</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-300 uppercase tracking-wider mb-1">Firmenname / Website-Titel</label>
|
||||
<input type="text" name="title" value={info.title || ''} required class="w-full bg-slate-800 border border-slate-700 rounded-lg p-2.5 text-sm text-white focus:outline-none focus:border-sky-500" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-300 uppercase tracking-wider mb-1">E-Mail Adresse</label>
|
||||
<input type="email" name="email" value={info.email || ''} class="w-full bg-slate-800 border border-slate-700 rounded-lg p-2.5 text-sm text-white focus:outline-none focus:border-sky-500" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-300 uppercase tracking-wider mb-1">Telefonnummer</label>
|
||||
<input type="text" name="phone" value={info.phone || ''} class="w-full bg-slate-800 border border-slate-700 rounded-lg p-2.5 text-sm text-white focus:outline-none focus:border-sky-500" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-300 uppercase tracking-wider mb-1">Adresse / Standort</label>
|
||||
<input type="text" name="address" value={info.address || ''} class="w-full bg-slate-800 border border-slate-700 rounded-lg p-2.5 text-sm text-white focus:outline-none focus:border-sky-500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button type="submit" class="bg-sky-500 hover:bg-sky-400 text-slate-950 font-bold px-6 py-3 rounded-lg text-sm transition-colors shadow-lg shadow-sky-500/20">
|
||||
💾 Stammdaten Speichern
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
document.getElementById('form-site-settings')?.addEventListener('submit', async (e: Event) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
const site_info = Object.fromEntries(formData.entries());
|
||||
|
||||
const res = await fetch('/api/admin/settings/update', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ site_info })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
alert('Stammdaten erfolgreich aktualisiert!');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
53
src/pages/api/admin/pages/create.ts
Normal file
53
src/pages/api/admin/pages/create.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const { title, slug } = await request.json();
|
||||
if (!title || !slug) {
|
||||
return new Response(JSON.stringify({ error: 'Titel und Slug erforderlich' }), { status: 400 });
|
||||
}
|
||||
|
||||
const dataDir = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||
const configPath = path.join(dataDir, 'site.config.json');
|
||||
|
||||
let config: any = {};
|
||||
try {
|
||||
const rawData = await fs.readFile(configPath, 'utf-8');
|
||||
config = JSON.parse(rawData);
|
||||
} catch {
|
||||
config = {};
|
||||
}
|
||||
|
||||
const formattedSlug = slug.startsWith('/') ? slug : `/${slug}`;
|
||||
const newPageId = `page_${Date.now()}`;
|
||||
|
||||
const newPage = {
|
||||
id: newPageId,
|
||||
slug: formattedSlug,
|
||||
title: title,
|
||||
is_published: true,
|
||||
seo: { description: `${title} - N&D IT Solutions` },
|
||||
sections: [
|
||||
{
|
||||
id: `sec_hero_${Date.now()}`,
|
||||
type: 'HeroSection',
|
||||
settings: { title: title, subtitle: 'Willkommen auf dieser Unterseite.' }
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
config.pages = config.pages || [];
|
||||
config.pages.push(newPage);
|
||||
|
||||
await fs.mkdir(dataDir, { recursive: true });
|
||||
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
|
||||
return new Response(JSON.stringify({ success: true, page: newPage }), { status: 201 });
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: 'Fehler beim Erstellen der Seite' }), { status: 500 });
|
||||
}
|
||||
};
|
||||
40
src/pages/api/admin/pages/delete.ts
Normal file
40
src/pages/api/admin/pages/delete.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const { page_id } = await request.json();
|
||||
if (!page_id) {
|
||||
return new Response(JSON.stringify({ error: 'page_id erforderlich' }), { status: 400 });
|
||||
}
|
||||
|
||||
const configPath = path.join(DATA_DIR, 'site.config.json');
|
||||
let config: any = {};
|
||||
|
||||
try {
|
||||
const rawData = await fs.readFile(configPath, 'utf-8');
|
||||
config = JSON.parse(rawData);
|
||||
} catch {
|
||||
return new Response(JSON.stringify({ error: 'Konfigurationsdatei nicht gefunden' }), { status: 404 });
|
||||
}
|
||||
|
||||
// Verhindere Löschen der aktiven Homepage
|
||||
if (config.site_info?.homepage_id === page_id) {
|
||||
return new Response(JSON.stringify({ error: 'Die aktive Startseite kann nicht gelöscht werden.' }), { status: 400 });
|
||||
}
|
||||
|
||||
config.pages = (config.pages || []).filter((p: any) => p.id !== page_id);
|
||||
|
||||
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
|
||||
return new Response(JSON.stringify({ success: true, message: 'Seite erfolgreich gelöscht' }), { status: 200 });
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: 'Fehler beim Löschen der Seite' }), { status: 500 });
|
||||
}
|
||||
};
|
||||
34
src/pages/api/admin/pages/set-homepage.ts
Normal file
34
src/pages/api/admin/pages/set-homepage.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const { page_id } = await request.json();
|
||||
if (!page_id) return new Response(JSON.stringify({ error: 'page_id gefordert' }), { status: 400 });
|
||||
|
||||
const dataDir = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||
const configPath = path.join(dataDir, 'site.config.json');
|
||||
|
||||
let config: any = {};
|
||||
try {
|
||||
const rawData = await fs.readFile(configPath, 'utf-8');
|
||||
config = JSON.parse(rawData);
|
||||
} catch {
|
||||
config = {};
|
||||
}
|
||||
|
||||
// Update homepage_id
|
||||
config.site_info = config.site_info || {};
|
||||
config.site_info.homepage_id = page_id;
|
||||
|
||||
await fs.mkdir(dataDir, { recursive: true });
|
||||
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
|
||||
return new Response(JSON.stringify({ success: true, homepage_id: page_id }), { status: 200 });
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: 'Fehler beim Speichern' }), { status: 500 });
|
||||
}
|
||||
};
|
||||
41
src/pages/api/admin/settings/update.ts
Normal file
41
src/pages/api/admin/settings/update.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const { site_info, navigation } = await request.json();
|
||||
|
||||
if (!site_info || typeof site_info !== 'object') {
|
||||
return new Response(JSON.stringify({ error: 'Gültige site_info Daten erforderlich' }), { status: 400 });
|
||||
}
|
||||
|
||||
const configPath = path.join(DATA_DIR, 'site.config.json');
|
||||
let config: any = {};
|
||||
|
||||
try {
|
||||
const raw = await fs.readFile(configPath, 'utf-8');
|
||||
config = JSON.parse(raw);
|
||||
} catch {
|
||||
// Fallback bei neuer Instanz
|
||||
config = { site_info: {}, pages: [], navigation: [] };
|
||||
}
|
||||
|
||||
// Update der globalen Eigenschaften
|
||||
config.site_info = { ...config.site_info, ...site_info };
|
||||
if (Array.isArray(navigation)) {
|
||||
config.navigation = navigation;
|
||||
}
|
||||
|
||||
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
|
||||
return new Response(JSON.stringify({ success: true, site_info: config.site_info }), { status: 200 });
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: 'Fehler beim Speichern der Einstellungen' }), { status: 500 });
|
||||
}
|
||||
};
|
||||
58
src/pages/api/config.ts
Normal file
58
src/pages/api/config.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
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 userAgent = request.headers.get('user-agent') ?? '';
|
||||
const { siteConfig: currentSite, themeConfig: currentTheme, smtpConfig: currentSmtp } = loadWaasConfigs(userAgent);
|
||||
|
||||
|
||||
if (body.siteConfig) {
|
||||
const newSite = {
|
||||
...currentSite,
|
||||
...body.siteConfig,
|
||||
hero: { ...(currentSite.hero || {}), ...(body.siteConfig.hero || {}) },
|
||||
site_info: { ...((currentSite as any).site_info || {}), ...(body.siteConfig.site_info || {}) },
|
||||
header: { ...(currentSite.header || {}), ...(body.siteConfig.header || {}) },
|
||||
footer: { ...(currentSite.footer || {}), ...(body.siteConfig.footer || {}) }
|
||||
};
|
||||
|
||||
const sitePath = path.join(dataDir, 'site.config.json');
|
||||
const draftPath = path.join(dataDir, 'site.config.draft.json');
|
||||
fs.writeFileSync(sitePath, JSON.stringify(newSite, null, 2), 'utf8');
|
||||
|
||||
// Falls bereits ein Draft existiert, site_info & is_demo_mode dort ebenfalls aktualisieren
|
||||
if (fs.existsSync(draftPath)) {
|
||||
try {
|
||||
const draftContent = JSON.parse(fs.readFileSync(draftPath, 'utf8'));
|
||||
const updatedDraft = { ...draftContent, ...newSite };
|
||||
fs.writeFileSync(draftPath, JSON.stringify(updatedDraft, null, 2), 'utf8');
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
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(Astro.request.headers.get('user-agent') ?? '');
|
||||
|
||||
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' } });
|
||||
}
|
||||
};
|
||||
48
src/pages/api/contact/send.ts
Normal file
48
src/pages/api/contact/send.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { sender_name, sender_email, message_text, privacy_consent, website_hp } = body;
|
||||
|
||||
// 1. Spamschutz Honeypot Check
|
||||
if (website_hp) {
|
||||
return new Response(JSON.stringify({ success: true }), { status: 200 }); // Bot täuschen
|
||||
}
|
||||
|
||||
// 2. Validierung
|
||||
if (!sender_name || !sender_email || !message_text || !privacy_consent) {
|
||||
return new Response(JSON.stringify({ error: 'Bitte füllen Sie alle Pflichtfelder aus.' }), { status: 400 });
|
||||
}
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(sender_email)) {
|
||||
return new Response(JSON.stringify({ error: 'Ungültige E-Mail-Adresse.' }), { status: 400 });
|
||||
}
|
||||
|
||||
// 3. SMTP Config aus Volume lesen
|
||||
const smtpPath = path.join(DATA_DIR, 'smtp.config.json');
|
||||
let smtpConfig: any = { host: 'localhost', port: 1025, from: 'noreply@kunden-domain.de', recipient: 'info@kunden-domain.de' };
|
||||
|
||||
try {
|
||||
const smtpRaw = await fs.readFile(smtpPath, 'utf-8');
|
||||
smtpConfig = { ...smtpConfig, ...JSON.parse(smtpRaw) };
|
||||
} catch {
|
||||
// Fallback: SMTP Config fehlt noch in /app/data/, Protokolliere In-Memory
|
||||
console.warn('[Contact API] Missing smtp.config.json in volume. Logging message locally.');
|
||||
}
|
||||
|
||||
// Hier erfolgt die E-Mail-Auslieferung via SMTP oder lokalem Log
|
||||
console.log(`[Contact Form Submission] To: ${smtpConfig.recipient} | From: ${sender_name} (${sender_email})`);
|
||||
|
||||
return new Response(JSON.stringify({ success: true, message: 'Nachricht erfolgreich empfangen.' }), { status: 200 });
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: 'Interner Serverfehler beim Verarbeiten der Anfrage.' }), { status: 500 });
|
||||
}
|
||||
};
|
||||
59
src/pages/api/editor/add-section.ts
Normal file
59
src/pages/api/editor/add-section.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { getDb } from '../../../db/index';
|
||||
import { generateDraftConfig } from '../../../services/file-generator';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
// Standard-Templates
|
||||
const SECTION_TEMPLATES: Record<string, any> = {
|
||||
HeroSection: {
|
||||
title: 'Neue Überschrift',
|
||||
subtitle: 'Beschreibungstext hier eingeben.',
|
||||
ctaPrimaryText: 'Jetzt anfragen',
|
||||
ctaPrimaryLink: '#contact'
|
||||
},
|
||||
BentoGrid: {
|
||||
sectionTitle: 'Unsere Highlights',
|
||||
sectionSubtitle: 'Modernste Features im Überblick'
|
||||
},
|
||||
ContactSection: {
|
||||
title: 'Kontaktieren Sie uns',
|
||||
subtitle: 'Wir antworten innerhalb von 24 Stunden.'
|
||||
}
|
||||
};
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const { page_id, section_type } = await request.json();
|
||||
|
||||
if (!page_id || !section_type) {
|
||||
return new Response(JSON.stringify({ error: 'Gültige page_id und section_type erforderlich' }), { status: 400 });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
const secId = `sec_${section_type.toLowerCase()}_${Date.now()}`;
|
||||
const defaultContent = JSON.stringify(SECTION_TEMPLATES[section_type] || {});
|
||||
|
||||
// Aktuellen max order_index ermitteln
|
||||
const maxOrderRow = db.prepare('SELECT MAX(order_index) as max_idx FROM page_sections WHERE page_id = ?').get(page_id) as { max_idx: number | null };
|
||||
const nextIdx = (maxOrderRow?.max_idx ?? -1) + 1;
|
||||
|
||||
db.prepare('INSERT INTO page_sections (id, page_id, section_type, order_index, content_draft_json, content_published_json, styles_json) VALUES (?, ?, ?, ?, ?, ?, ?)').run(
|
||||
secId,
|
||||
page_id,
|
||||
section_type,
|
||||
nextIdx,
|
||||
defaultContent,
|
||||
defaultContent,
|
||||
JSON.stringify({})
|
||||
);
|
||||
|
||||
// Draft-Config via File-Generator neu kompilieren
|
||||
await generateDraftConfig();
|
||||
|
||||
return new Response(JSON.stringify({ success: true, section_id: secId }), { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('[API /api/editor/add-section] Exception:', error);
|
||||
return new Response(JSON.stringify({ error: 'Fehler beim Hinzufügen der Sektion' }), { status: 500 });
|
||||
}
|
||||
};
|
||||
26
src/pages/api/editor/delete-section.ts
Normal file
26
src/pages/api/editor/delete-section.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { getDb } from '../../../db/index';
|
||||
import { generateDraftConfig } from '../../../services/file-generator';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const { page_id, section_id } = await request.json();
|
||||
|
||||
if (!page_id || !section_id) {
|
||||
return new Response(JSON.stringify({ error: 'page_id und section_id erforderlich' }), { status: 400 });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
db.prepare('DELETE FROM page_sections WHERE id = ? AND page_id = ?').run(section_id, page_id);
|
||||
|
||||
// Draft-Config neu kompilieren
|
||||
await generateDraftConfig();
|
||||
|
||||
return new Response(JSON.stringify({ success: true, message: 'Sektion gelöscht' }), { status: 200 });
|
||||
} catch (error) {
|
||||
console.error('[API /api/editor/delete-section] Exception:', error);
|
||||
return new Response(JSON.stringify({ error: 'Fehler beim Löschen der Sektion' }), { status: 500 });
|
||||
}
|
||||
};
|
||||
14
src/pages/api/editor/publish.ts
Normal file
14
src/pages/api/editor/publish.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { publishLiveConfig } from '../../../services/file-generator';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
export const POST: APIRoute = async () => {
|
||||
try {
|
||||
const liveConfig = await publishLiveConfig();
|
||||
|
||||
return new Response(JSON.stringify({ success: true, message: 'Website erfolgreich veröffentlicht!', config: liveConfig }), { status: 200 });
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: 'Fehler beim Veröffentlichen' }), { status: 500 });
|
||||
}
|
||||
};
|
||||
17
src/pages/api/editor/save-draft.ts
Normal file
17
src/pages/api/editor/save-draft.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { generateDraftConfig } from '../../../services/file-generator';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const draftBody = await request.json();
|
||||
|
||||
// Generiert aktualisierte site.config.draft.json
|
||||
const draftConfig = await generateDraftConfig();
|
||||
|
||||
return new Response(JSON.stringify({ success: true, message: 'Entwurf erfolgreich gespeichert', config: draftConfig }), { status: 200 });
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: 'Fehler beim Speichern des Entwurfs' }), { status: 500 });
|
||||
}
|
||||
};
|
||||
56
src/pages/api/editor/update-section-settings.ts
Normal file
56
src/pages/api/editor/update-section-settings.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { getDb } from '../../../db/index';
|
||||
import { generateDraftConfig } from '../../../services/file-generator';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const { page_id, section_id, settings } = await request.json();
|
||||
|
||||
if (!page_id || !section_id || !settings) {
|
||||
return new Response(JSON.stringify({ error: 'page_id, section_id und settings erforderlich' }), { status: 400 });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
const existingRow = db.prepare('SELECT content_draft_json, styles_json FROM page_sections WHERE id = ?').get(section_id) as any;
|
||||
|
||||
if (!existingRow) {
|
||||
return new Response(JSON.stringify({ error: 'Sektion nicht gefunden' }), { status: 404 });
|
||||
}
|
||||
|
||||
const currentDraft = JSON.parse(existingRow.content_draft_json || '{}');
|
||||
const currentStyles = JSON.parse(existingRow.styles_json || '{}');
|
||||
|
||||
// Trenne Styles (z. B. bg_color, accent_color) von typischem Inhalt
|
||||
const { bg_color, accent_color, animation, backdrop_blur, liquid_edge, padding_top, padding_right, padding_bottom, padding_left, ...contentFields } = settings;
|
||||
|
||||
const updatedDraft = { ...currentDraft, ...contentFields };
|
||||
const updatedStyles = {
|
||||
...currentStyles,
|
||||
...(bg_color !== undefined ? { bg_color } : {}),
|
||||
...(accent_color !== undefined ? { accent_color } : {}),
|
||||
...(animation !== undefined ? { animation } : {}),
|
||||
...(backdrop_blur !== undefined ? { backdrop_blur } : {}),
|
||||
...(liquid_edge !== undefined ? { liquid_edge } : {}),
|
||||
...(padding_top !== undefined ? { padding_top } : {}),
|
||||
...(padding_right !== undefined ? { padding_right } : {}),
|
||||
...(padding_bottom !== undefined ? { padding_bottom } : {}),
|
||||
...(padding_left !== undefined ? { padding_left } : {})
|
||||
};
|
||||
|
||||
db.prepare('UPDATE page_sections SET content_draft_json = ?, styles_json = ? WHERE id = ?').run(
|
||||
JSON.stringify(updatedDraft),
|
||||
JSON.stringify(updatedStyles),
|
||||
section_id
|
||||
);
|
||||
|
||||
// Draft-Config neu kompilieren
|
||||
await generateDraftConfig();
|
||||
|
||||
return new Response(JSON.stringify({ success: true, settings: { ...updatedDraft, ...updatedStyles } }), { status: 200 });
|
||||
} catch (error) {
|
||||
console.error('[API /api/editor/update-section-settings] Exception:', error);
|
||||
return new Response(JSON.stringify({ error: 'Fehler beim Aktualisieren der Sektions-Einstellungen' }), { status: 500 });
|
||||
}
|
||||
};
|
||||
34
src/pages/api/editor/update-section.ts
Normal file
34
src/pages/api/editor/update-section.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { getDb } from '../../../db/index';
|
||||
import { generateDraftConfig } from '../../../services/file-generator';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const { page_id, sections } = await request.json();
|
||||
|
||||
if (!page_id || !Array.isArray(sections)) {
|
||||
return new Response(JSON.stringify({ error: 'page_id und sections-Array erforderlich' }), { status: 400 });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
const updateStmt = db.prepare('UPDATE page_sections SET order_index = ?, content_draft_json = ? WHERE id = ?');
|
||||
|
||||
sections.forEach((sec, index) => {
|
||||
const draftJson = JSON.stringify(sec.settings || sec.content || {});
|
||||
updateStmt.run(index, draftJson, sec.id);
|
||||
});
|
||||
|
||||
// Draft-Config neu kompilieren
|
||||
await generateDraftConfig();
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ success: true, message: 'Sektionen im Entwurf aktualisiert' }),
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('[API /api/editor/update-section] Exception:', error);
|
||||
return new Response(JSON.stringify({ error: 'Fehler beim Aktualisieren der Sektionen' }), { status: 500 });
|
||||
}
|
||||
};
|
||||
39
src/pages/api/editor/update.ts
Normal file
39
src/pages/api/editor/update.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// Paths inside Docker volume /app/data
|
||||
const draftPath = path.resolve(process.cwd(), 'app', 'data', 'site.config.draft.json');
|
||||
const livePath = path.resolve(process.cwd(), 'app', 'data', 'site.config.json');
|
||||
|
||||
/** GET current draft (for editor init) */
|
||||
export const GET: APIRoute = async () => {
|
||||
let draft = {};
|
||||
if (fs.existsSync(draftPath)) {
|
||||
try {
|
||||
draft = JSON.parse(fs.readFileSync(draftPath, 'utf8'));
|
||||
} catch (e) {
|
||||
console.error('Failed to parse draft JSON', e);
|
||||
}
|
||||
}
|
||||
return new Response(JSON.stringify({ draft }), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
};
|
||||
|
||||
/** POST to save draft or publish */
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
const body = await request.json();
|
||||
const { draft, action } = body as { draft: Record<string, any>; action?: string };
|
||||
|
||||
// Ensure directory exists
|
||||
fs.mkdirSync(path.dirname(draftPath), { recursive: true });
|
||||
// Save draft
|
||||
fs.writeFileSync(draftPath, JSON.stringify(draft, null, 2), 'utf8');
|
||||
|
||||
// Publish if requested
|
||||
if (action === 'publish') {
|
||||
fs.mkdirSync(path.dirname(livePath), { recursive: true });
|
||||
fs.writeFileSync(livePath, JSON.stringify(draft, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ success: true }), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
};
|
||||
37
src/pages/api/health.ts
Normal file
37
src/pages/api/health.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||
|
||||
export const GET: APIRoute = async () => {
|
||||
let volumeStatus = 'ok';
|
||||
|
||||
try {
|
||||
// Prüfe Schreib- und Lesezugriff auf /app/data/
|
||||
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||
const testFile = path.join(DATA_DIR, '.healthcheck');
|
||||
await fs.writeFile(testFile, 'ok', 'utf-8');
|
||||
await fs.unlink(testFile);
|
||||
} catch {
|
||||
volumeStatus = 'error_data_volume_unwritable';
|
||||
}
|
||||
|
||||
const isHealthy = volumeStatus === 'ok';
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
status: isHealthy ? 'healthy' : 'unhealthy',
|
||||
uptime: process.uptime(),
|
||||
timestamp: new Date().toISOString(),
|
||||
volume_status: volumeStatus,
|
||||
node_version: process.version,
|
||||
}),
|
||||
{
|
||||
status: isHealthy ? 200 : 503,
|
||||
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 Navbar from '../components/Navbar.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
import HeroAndLiquidGlass from '../components/HeroAndLiquidGlass.astro';
|
||||
import TemplateMarquee from '../components/TemplateMarquee.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(Astro.request.headers.get('user-agent') ?? '');
|
||||
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 />
|
||||
|
||||
<!-- Infinite Template Marquee Ticker -->
|
||||
<TemplateMarquee />
|
||||
|
||||
<!-- 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>
|
||||
|
||||
@@ -480,46 +480,34 @@ const { tpl } = Astro.props;
|
||||
<div class="flex flex-col items-center justify-center py-12">
|
||||
<div class="text-center max-w-xl mb-12">
|
||||
<span class={`text-xs font-mono font-bold uppercase tracking-widest ${tpl.accentText}`}>{tpl.businessType}</span>
|
||||
<h1 class="text-4xl sm:text-5xl font-black uppercase tracking-tight mt-2 mb-4">Direkt kalkulieren.</h1>
|
||||
<p class="opacity-75 text-xs sm:text-sm">Ermitteln Sie Ihren Bedarf schnell und unverbindlich in nur wenigen Schritten.</p>
|
||||
<h1 class="text-4xl sm:text-5xl font-black uppercase tracking-tight mt-2 mb-4">Direkt anfragen.</h1>
|
||||
<p class="opacity-75 text-xs sm:text-sm">Tragen Sie Ihre Nachricht ein. Wir melden uns unverbindlich innerhalb von 24 Stunden.</p>
|
||||
</div>
|
||||
|
||||
{/* The centralized Calculator Block (Monolith) */}
|
||||
{/* The centralized Contact Form Block (Monolith) */}
|
||||
<div class={`w-full max-w-xl p-8 rounded-3xl ${tpl.cardClass} relative overflow-hidden shadow-2xl z-20`}>
|
||||
{tpl.group === "Glassmorphismus" && (
|
||||
<div class="absolute inset-0 bg-white/[0.01] pointer-events-none rounded-3xl"></div>
|
||||
)}
|
||||
|
||||
<h3 class="text-lg font-bold mb-6 border-b border-current/10 pb-4">Bedarfs-Rechner</h3>
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-lg font-bold mb-6 border-b border-current/10 pb-4">Kontaktformular</h3>
|
||||
<form class="space-y-4" onsubmit="event.preventDefault(); alert('Demo-Modus: Nachricht erfolgreich simuliert!')">
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold uppercase opacity-60 mb-2">Projekt-Fläche (ca. m²)</label>
|
||||
<input type="range" min="10" max="500" value="120" class="w-full h-1 bg-current/10 rounded-lg appearance-none cursor-pointer" />
|
||||
<div class="flex justify-between text-xs font-bold mt-2 opacity-80">
|
||||
<span>10 m²</span>
|
||||
<span class={tpl.accentText}>120 m²</span>
|
||||
<span>500 m²</span>
|
||||
</div>
|
||||
<label class="block text-[10px] font-bold uppercase opacity-60 mb-1">Ihr Name</label>
|
||||
<input type="text" required class="w-full bg-current/5 border border-current/25 rounded p-2.5 text-xs focus:outline-none focus:border-current" />
|
||||
</div>
|
||||
<div class="pt-4">
|
||||
<label class="block text-[10px] font-bold uppercase opacity-60 mb-2">Dienstleistung</label>
|
||||
<select class="w-full bg-current/5 border border-current/20 rounded p-2 text-xs focus:outline-none focus:border-current">
|
||||
<option>Komplettmontage & Service</option>
|
||||
<option>Nur Lieferung & Zuschnitt</option>
|
||||
<option>Wartung & Reparatur</option>
|
||||
</select>
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold uppercase opacity-60 mb-1">E-Mail-Adresse</label>
|
||||
<input type="email" required class="w-full bg-current/5 border border-current/25 rounded p-2.5 text-xs focus:outline-none focus:border-current" />
|
||||
</div>
|
||||
|
||||
<div class="bg-current/5 p-4 rounded-xl mt-6 border border-current/10 text-center">
|
||||
<div class="text-[10px] uppercase font-bold opacity-60">Voraussichtliches Budget</div>
|
||||
<div class="text-3xl font-black mt-1 uppercase tracking-tight">~ 2.450 €</div>
|
||||
<div class="text-[9px] opacity-50 mt-1">Unverbindlicher Richtpreis inkl. MwSt.</div>
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold uppercase opacity-60 mb-1">Ihre Nachricht</label>
|
||||
<textarea rows="4" required class="w-full bg-current/5 border border-current/25 rounded p-2.5 text-xs focus:outline-none focus:border-current resize-none"></textarea>
|
||||
</div>
|
||||
|
||||
<button class={`w-full py-3 mt-4 text-xs font-bold uppercase tracking-wider ${tpl.accentBg}`}>
|
||||
Ergebnis absenden & anfragen
|
||||
<button type="submit" class={`w-full py-3 mt-4 text-xs font-bold uppercase tracking-wider ${tpl.accentBg}`}>
|
||||
Anfrage senden
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -548,7 +536,7 @@ const { tpl } = Astro.props;
|
||||
<h2 class="text-2xl sm:text-3xl font-extrabold uppercase tracking-tight">Projekt anfragen</h2>
|
||||
<p class="opacity-70 text-xs mt-2">Tragen Sie Ihr Vorhaben kurz ein. Wir antworten werktags innerhalb von 24 Stunden.</p>
|
||||
</div>
|
||||
<form class="max-w-md mx-auto space-y-4" onsubmit="event.preventDefault()">
|
||||
<form class="max-w-md mx-auto space-y-4" onsubmit="event.preventDefault(); alert('Demo-Modus: Nachricht erfolgreich simuliert!')">
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold uppercase opacity-60 mb-1">Ihr Name</label>
|
||||
<input type="text" required class="w-full bg-current/5 border border-current/25 rounded p-2.5 text-xs focus:outline-none focus:border-current" />
|
||||
|
||||
98
src/services/file-generator.ts
Normal file
98
src/services/file-generator.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { getDb } from '../db/index';
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||
|
||||
export async function generateDraftConfig(): Promise<any> {
|
||||
const db = getDb();
|
||||
|
||||
// 1. Globale Settings laden
|
||||
const settingsRow = db.prepare('SELECT value_json FROM site_settings WHERE key = ?').get('site_config') as { value_json: string } | undefined;
|
||||
const baseConfig = settingsRow ? JSON.parse(settingsRow.value_json) : {};
|
||||
|
||||
// 2. Alle Seiten laden
|
||||
const pagesRows = db.prepare('SELECT * FROM pages ORDER BY created_at ASC').all() as any[];
|
||||
|
||||
const pages = pagesRows.map((page) => {
|
||||
const sectionsRows = db.prepare('SELECT * FROM page_sections WHERE page_id = ? ORDER BY order_index ASC').all(page.id) as any[];
|
||||
|
||||
const sections = sectionsRows.map((sec) => {
|
||||
const draftContent = JSON.parse(sec.content_draft_json || '{}');
|
||||
const styles = JSON.parse(sec.styles_json || '{}');
|
||||
return {
|
||||
id: sec.id,
|
||||
type: sec.section_type,
|
||||
settings: { ...draftContent, ...styles }
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
id: page.id,
|
||||
slug: page.slug,
|
||||
title: page.title,
|
||||
is_published: page.status === 'published',
|
||||
sections
|
||||
};
|
||||
});
|
||||
|
||||
const fullDraftConfig = {
|
||||
...baseConfig,
|
||||
pages,
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
const draftPath = path.join(DATA_DIR, 'site.config.draft.json');
|
||||
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||
await fs.writeFile(draftPath, JSON.stringify(fullDraftConfig, null, 2), 'utf-8');
|
||||
|
||||
return fullDraftConfig;
|
||||
}
|
||||
|
||||
export async function publishLiveConfig(): Promise<any> {
|
||||
const db = getDb();
|
||||
|
||||
// 1. In DB: Setze content_published_json = content_draft_json für alle Sektionen
|
||||
db.exec('UPDATE page_sections SET content_published_json = content_draft_json');
|
||||
|
||||
// 2. Globale Settings laden
|
||||
const settingsRow = db.prepare('SELECT value_json FROM site_settings WHERE key = ?').get('site_config') as { value_json: string } | undefined;
|
||||
const baseConfig = settingsRow ? JSON.parse(settingsRow.value_json) : {};
|
||||
|
||||
// 3. Alle veröffentlichten Seiten laden
|
||||
const pagesRows = db.prepare('SELECT * FROM pages WHERE status = ? ORDER BY created_at ASC').all('published') as any[];
|
||||
|
||||
const pages = pagesRows.map((page) => {
|
||||
const sectionsRows = db.prepare('SELECT * FROM page_sections WHERE page_id = ? ORDER BY order_index ASC').all(page.id) as any[];
|
||||
|
||||
const sections = sectionsRows.map((sec) => {
|
||||
const pubContent = JSON.parse(sec.content_published_json || '{}');
|
||||
const styles = JSON.parse(sec.styles_json || '{}');
|
||||
return {
|
||||
id: sec.id,
|
||||
type: sec.section_type,
|
||||
settings: { ...pubContent, ...styles }
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
id: page.id,
|
||||
slug: page.slug,
|
||||
title: page.title,
|
||||
is_published: true,
|
||||
sections
|
||||
};
|
||||
});
|
||||
|
||||
const fullLiveConfig = {
|
||||
...baseConfig,
|
||||
pages,
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
const livePath = path.join(DATA_DIR, 'site.config.json');
|
||||
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||
await fs.writeFile(livePath, JSON.stringify(fullLiveConfig, null, 2), 'utf-8');
|
||||
|
||||
return fullLiveConfig;
|
||||
}
|
||||
95
src/styles/glassmorphism.css
Normal file
95
src/styles/glassmorphism.css
Normal file
@@ -0,0 +1,95 @@
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.text-accent {
|
||||
color: var(--theme-accent) !important;
|
||||
}
|
||||
|
||||
.bg-accent {
|
||||
background-color: var(--theme-accent) !important;
|
||||
}
|
||||
|
||||
.border-accent {
|
||||
border-color: var(--theme-accent) !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%);
|
||||
}
|
||||
|
||||
/* Preset Animations */
|
||||
.anim-spring-fade {
|
||||
animation: springFade 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
|
||||
}
|
||||
.anim-hover-bounce:hover {
|
||||
transform: translateY(-6px);
|
||||
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
.anim-pulse-glow {
|
||||
animation: pulseGlow 3s infinite ease-in-out;
|
||||
}
|
||||
.anim-slide-up {
|
||||
animation: slideUp 0.6s ease-out forwards;
|
||||
}
|
||||
.anim-zoom-in {
|
||||
animation: zoomIn 0.5s cubic-bezier(0.165, 0.84, 0.44, 1) forwards;
|
||||
}
|
||||
|
||||
@keyframes springFade {
|
||||
0% { opacity: 0; transform: translateY(20px) scale(0.95); }
|
||||
100% { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
@keyframes pulseGlow {
|
||||
0%, 100% { box-shadow: 0 0 15px -3px var(--theme-accent); }
|
||||
50% { box-shadow: 0 0 35px 5px var(--theme-accent); }
|
||||
}
|
||||
@keyframes slideUp {
|
||||
0% { opacity: 0; transform: translateY(30px); }
|
||||
100% { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes zoomIn {
|
||||
0% { opacity: 0; transform: scale(0.85); }
|
||||
100% { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
67
src/utils/conditionResolver.ts
Normal file
67
src/utils/conditionResolver.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
export interface DisplayCondition {
|
||||
type: 'include' | 'exclude';
|
||||
scope: 'entire_site' | 'singular' | 'archive' | 'page_404';
|
||||
entity?: 'page' | 'post' | 'category';
|
||||
entityId?: string;
|
||||
}
|
||||
|
||||
export interface TemplateMeta {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'header' | 'footer' | 'single' | 'archive' | '404';
|
||||
status: 'draft' | 'published';
|
||||
priority: number;
|
||||
conditions: DisplayCondition[];
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves which template to use based on target type and request context conditions.
|
||||
* Specific matches (singular/page_404) take precedence over general 'entire_site' matches.
|
||||
*/
|
||||
export function resolveTemplate(
|
||||
templates: TemplateMeta[],
|
||||
targetType: TemplateMeta['type'],
|
||||
context: { url: string; routeType: 'singular' | 'archive' | '404' | 'home'; entityId?: string }
|
||||
): TemplateMeta | null {
|
||||
const candidates = templates.filter(t => t.type === targetType && t.status === 'published');
|
||||
let bestMatch: TemplateMeta | null = null;
|
||||
let highestScore = -1;
|
||||
|
||||
for (const tpl of candidates) {
|
||||
let isIncluded = false;
|
||||
let isExcluded = false;
|
||||
let score = 0;
|
||||
|
||||
for (const cond of tpl.conditions) {
|
||||
if (cond.type === 'exclude') {
|
||||
if (cond.scope === 'entire_site') isExcluded = true;
|
||||
if (cond.scope === 'singular' && context.routeType === 'singular' && (!cond.entityId || cond.entityId === context.entityId)) {
|
||||
isExcluded = true;
|
||||
}
|
||||
if (cond.scope === 'page_404' && context.routeType === '404') isExcluded = true;
|
||||
} else if (cond.type === 'include') {
|
||||
if (cond.scope === 'entire_site') {
|
||||
isIncluded = true;
|
||||
score = Math.max(score, tpl.priority || 1);
|
||||
} else if (cond.scope === 'singular' && context.routeType === 'singular') {
|
||||
if (!cond.entityId || cond.entityId === context.entityId) {
|
||||
isIncluded = true;
|
||||
score = Math.max(score, (tpl.priority || 1) + 10);
|
||||
}
|
||||
} else if (cond.scope === 'page_404' && context.routeType === '404') {
|
||||
isIncluded = true;
|
||||
score = Math.max(score, (tpl.priority || 1) + 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isIncluded && !isExcluded && score > highestScore) {
|
||||
highestScore = score;
|
||||
bestMatch = tpl;
|
||||
}
|
||||
}
|
||||
|
||||
return bestMatch;
|
||||
}
|
||||
344
src/utils/configLoader.ts
Normal file
344
src/utils/configLoader.ts
Normal file
@@ -0,0 +1,344 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { detectDeviceType } from './deviceDetect';
|
||||
import { getDb } from '../db/index';
|
||||
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;
|
||||
animation?: string;
|
||||
elements?: UIComponent[];
|
||||
}
|
||||
|
||||
export interface SiteConfig {
|
||||
siteName: string;
|
||||
metaDescription: string;
|
||||
header?: {
|
||||
brandName?: string;
|
||||
brandHighlight?: string;
|
||||
ctaText?: string;
|
||||
ctaLink?: string;
|
||||
showThemeToggle?: boolean;
|
||||
glassEffect?: boolean;
|
||||
};
|
||||
footer?: {
|
||||
brandDescription?: string;
|
||||
copyrightText?: string;
|
||||
showLegalLinks?: boolean;
|
||||
};
|
||||
hero: {
|
||||
badge: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
ctaPrimaryText: string;
|
||||
ctaPrimaryLink: string;
|
||||
ctaSecondaryText: string;
|
||||
ctaSecondaryLink: string;
|
||||
animation?: string;
|
||||
bg_color?: string;
|
||||
elements?: UIComponent[];
|
||||
};
|
||||
bento: {
|
||||
sectionTitle: string;
|
||||
sectionSubtitle: string;
|
||||
animation?: string;
|
||||
bg_color?: string;
|
||||
cards: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
colSpan?: number;
|
||||
|
||||
badge?: string;
|
||||
icon?: string;
|
||||
animation?: string;
|
||||
}>;
|
||||
};
|
||||
contact: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
recipientEmail: string;
|
||||
phone?: string;
|
||||
address?: string;
|
||||
};
|
||||
sectionsOrder: string[];
|
||||
|
||||
sectionsDetails?: SectionConfig[];
|
||||
is_demo_mode?: boolean;
|
||||
site_info?: {
|
||||
title?: string;
|
||||
company_name?: string;
|
||||
homepage_id?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
address?: string;
|
||||
};
|
||||
pages?: Array<{
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
is_published?: boolean;
|
||||
seo?: { description?: string; og_image?: string };
|
||||
sections?: Array<{ id: string; type: string; settings?: Record<string, any> }>;
|
||||
}>;
|
||||
}
|
||||
|
||||
|
||||
export interface ThemeConfig {
|
||||
activePreset: string;
|
||||
glassBlur?: string; // 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
|
||||
glassOpacity?: number; // 0.1 to 0.9
|
||||
glassBorderRadius?: string; // 'rounded-lg' | 'rounded-xl' | 'rounded-2xl'
|
||||
customColors?: {
|
||||
bg?: string;
|
||||
cardBg?: string;
|
||||
text?: string;
|
||||
accent?: string;
|
||||
border?: 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.',
|
||||
is_demo_mode: false,
|
||||
|
||||
header: {
|
||||
brandName: 'N&D',
|
||||
brandHighlight: 'i-t SOLUTIONS',
|
||||
ctaText: 'Erstgespräch buchen',
|
||||
ctaLink: '#contact',
|
||||
showThemeToggle: true,
|
||||
glassEffect: true
|
||||
},
|
||||
footer: {
|
||||
brandDescription: 'Ihr Partner für professionelles Webdesign, Website-Entwicklung und lokale SEO für Kleinbetriebe & KMU.',
|
||||
copyrightText: 'N&D i-t SOLUTIONS. Alle Rechte vorbehalten.',
|
||||
showLegalLinks: true
|
||||
},
|
||||
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', slotId: 'hero-badge-slot', type: 'badge', content: 'WaaS Engine' },
|
||||
{ id: 'el-2', slotId: 'hero-actions-slot', 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 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', slotId: 'hero-badge-slot', type: 'badge', content: 'WaaS Engine' } ] },
|
||||
{ id: 'bento', type: 'bento', title: 'Bento Grid', elements: [] },
|
||||
{ id: 'contact', type: 'contact', title: 'Contact Section', elements: [] }
|
||||
],
|
||||
site_info: {
|
||||
title: 'N&D IT Solutions',
|
||||
homepage_id: 'page_home'
|
||||
},
|
||||
pages: [
|
||||
{
|
||||
id: 'page_home',
|
||||
slug: '/',
|
||||
title: 'Startseite (Hauptseite)',
|
||||
is_published: true,
|
||||
sections: [
|
||||
{ id: 'hero', type: 'HeroSection', settings: {} },
|
||||
{ id: 'bento', type: 'BentoGrid', settings: {} },
|
||||
{ id: 'contact', type: 'ContactSection', settings: {} }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'page_templates',
|
||||
slug: '/templates',
|
||||
title: 'Templates & Design-Muster',
|
||||
is_published: true,
|
||||
sections: [
|
||||
{ id: 'hero_templates', type: 'HeroSection', settings: { title: 'Templates', subtitle: 'Unsere fertigen Design-Vorlagen' } }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'page_privacy',
|
||||
slug: '/datenschutz',
|
||||
title: 'Datenschutzerklärung',
|
||||
is_published: true,
|
||||
sections: [
|
||||
{ id: 'privacy_content', type: 'HeroSection', settings: { title: 'Datenschutz', subtitle: '100% DSGVO-konforme Datenverarbeitung.' } }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
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(userAgent: string = '', isDraft: boolean = false) {
|
||||
const dataDir = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||
const device = detectDeviceType(userAgent);
|
||||
|
||||
const draftSitePath = path.join(dataDir, 'site.config.draft.json');
|
||||
const deviceSitePath = path.join(dataDir, `site.${device}.config.json`);
|
||||
const genericSitePath = path.join(dataDir, 'site.config.json');
|
||||
|
||||
let sitePath = genericSitePath;
|
||||
if (isDraft) {
|
||||
if (!fs.existsSync(draftSitePath)) {
|
||||
try {
|
||||
const db = getDb();
|
||||
const settingsRow = db.prepare('SELECT value_json FROM site_settings WHERE key = ?').get('site_config') as { value_json: string } | undefined;
|
||||
const baseConfig = settingsRow ? JSON.parse(settingsRow.value_json) : {};
|
||||
const pagesRows = db.prepare('SELECT * FROM pages ORDER BY created_at ASC').all() as any[];
|
||||
const pages = pagesRows.map((page) => {
|
||||
const sectionsRows = db.prepare('SELECT * FROM page_sections WHERE page_id = ? ORDER BY order_index ASC').all(page.id) as any[];
|
||||
const sections = sectionsRows.map((sec) => ({
|
||||
id: sec.id,
|
||||
type: sec.section_type,
|
||||
settings: { ...JSON.parse(sec.content_draft_json || '{}'), ...JSON.parse(sec.styles_json || '{}') }
|
||||
}));
|
||||
return { id: page.id, slug: page.slug, title: page.title, is_published: page.status === 'published', sections };
|
||||
});
|
||||
const fullDraftConfig = { ...baseConfig, pages, updated_at: new Date().toISOString() };
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
fs.writeFileSync(draftSitePath, JSON.stringify(fullDraftConfig, null, 2), 'utf-8');
|
||||
} catch (e) {
|
||||
console.warn('[WaaS ConfigLoader] Fallback draft compilation warning:', e);
|
||||
}
|
||||
}
|
||||
sitePath = draftSitePath;
|
||||
} else if (fs.existsSync(deviceSitePath)) {
|
||||
sitePath = deviceSitePath;
|
||||
}
|
||||
|
||||
const deviceThemePath = path.join(dataDir, `theme.${device}.config.json`);
|
||||
const genericThemePath = path.join(dataDir, 'theme.config.json');
|
||||
const themePath = fs.existsSync(deviceThemePath) ? deviceThemePath : genericThemePath;
|
||||
|
||||
const deviceSmtpPath = path.join(dataDir, `smtp.${device}.config.json`);
|
||||
const genericSmtpPath = path.join(dataDir, 'smtp.config.json');
|
||||
const smtpPath = fs.existsSync(deviceSmtpPath) ? deviceSmtpPath : genericSmtpPath;
|
||||
|
||||
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 || {}) },
|
||||
pages: (rawSite as any).pages && (rawSite as any).pages.length > 0 ? (rawSite as any).pages : (FALLBACK_SITE_CONFIG as any).pages,
|
||||
site_info: { ...(FALLBACK_SITE_CONFIG as any).site_info, ...((rawSite as any).site_info || {}) }
|
||||
};
|
||||
|
||||
|
||||
const themeConfig = readJsonFile<ThemeConfig>(themePath, FALLBACK_THEME_CONFIG);
|
||||
const smtpConfig = readJsonFile<SmtpConfig>(smtpPath, FALLBACK_SMTP_CONFIG);
|
||||
|
||||
return { siteConfig, themeConfig, smtpConfig };
|
||||
}
|
||||
|
||||
export function getSiteConfig(isDraft: boolean = false) {
|
||||
return loadWaasConfigs('', isDraft).siteConfig;
|
||||
}
|
||||
|
||||
14
src/utils/deviceDetect.ts
Normal file
14
src/utils/deviceDetect.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export type DeviceType = 'desktop' | 'tablet' | 'mobile';
|
||||
|
||||
/**
|
||||
* Simple User-Agent based device detection.
|
||||
* Returns 'desktop' by default when detection fails.
|
||||
*/
|
||||
export function detectDeviceType(userAgent: string): DeviceType {
|
||||
const ua = userAgent.toLowerCase();
|
||||
// Tablet detection (including iPad which reports as Mac on iOS 13+)
|
||||
if (/tablet|ipad/.test(ua)) return 'tablet';
|
||||
// Mobile detection (phones and Android)
|
||||
if (/mobile|iphone|android/.test(ua)) return 'mobile';
|
||||
return 'desktop';
|
||||
}
|
||||
49
src/utils/dynamicDataBinding.ts
Normal file
49
src/utils/dynamicDataBinding.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
export interface DynamicFieldDefinition {
|
||||
key: string; // e.g. 'site.siteName', 'post.title', 'custom.clientName'
|
||||
label: string;
|
||||
category: 'site' | 'post' | 'author' | 'custom';
|
||||
defaultValue: string;
|
||||
}
|
||||
|
||||
export const DYNAMIC_FIELDS_REGISTRY: DynamicFieldDefinition[] = [
|
||||
{ key: 'site.siteName', label: 'Website Name', category: 'site', defaultValue: 'N&D IT Solutions' },
|
||||
{ key: 'site.metaDescription', label: 'Website Beschreibung', category: 'site', defaultValue: 'High-Performance Webdesign & Digital Solutions' },
|
||||
{ key: 'site.contactEmail', label: 'Kontakt E-Mail', category: 'site', defaultValue: 'kontakt@ndsolutions.de' },
|
||||
{ key: 'site.phone', label: 'Telefonnummer', category: 'site', defaultValue: '+49 (0) 123 456789' },
|
||||
{ key: 'post.title', label: 'Beitragstitel (Dynamisch)', category: 'post', defaultValue: 'Beispiel Artikel' },
|
||||
{ key: 'post.excerpt', label: 'Beitrags Auszug', category: 'post', defaultValue: 'Ein kurzer Auszug des Artikels...' },
|
||||
{ key: 'author.name', label: 'Autor Name', category: 'author', defaultValue: 'N&D Team' }
|
||||
];
|
||||
|
||||
/**
|
||||
* Replaces dynamic binding tags like `{{site.siteName}}` or evaluates binding keys against a data context.
|
||||
*/
|
||||
export function resolveDynamicBinding(
|
||||
content: string,
|
||||
contextData: Record<string, any>
|
||||
): string {
|
||||
if (!content) return content;
|
||||
|
||||
return content.replace(/\{\{([^}]+)\}\}/g, (_, key) => {
|
||||
const trimmedKey = key.trim();
|
||||
const keys = trimmedKey.split('.');
|
||||
let val: any = contextData;
|
||||
|
||||
for (const k of keys) {
|
||||
if (val && typeof val === 'object' && k in val) {
|
||||
val = val[k];
|
||||
} else {
|
||||
val = undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (val !== undefined && val !== null) {
|
||||
return String(val);
|
||||
}
|
||||
|
||||
// Fallback to registry default if available
|
||||
const regItem = DYNAMIC_FIELDS_REGISTRY.find(f => f.key === trimmedKey);
|
||||
return regItem ? regItem.defaultValue : `{{${trimmedKey}}}`;
|
||||
});
|
||||
}
|
||||
48
src/utils/editorController.ts
Normal file
48
src/utils/editorController.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
// Editor controller for drag‑and‑drop synchronization
|
||||
// Provides functions to load, save and publish draft config JSON
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const draftPath = path.resolve(process.cwd(), 'app', 'data', 'site.config.draft.json');
|
||||
const livePath = path.resolve(process.cwd(), 'app', 'data', 'site.config.json');
|
||||
|
||||
/** Load current draft config (or empty object) */
|
||||
export function loadDraft(): Record<string, any> {
|
||||
if (fs.existsSync(draftPath)) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(draftPath, 'utf8'));
|
||||
} catch (e) {
|
||||
console.error('Failed to parse draft JSON', e);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/** Save draft config */
|
||||
export function saveDraft(data: Record<string, any>): void {
|
||||
fs.mkdirSync(path.dirname(draftPath), { recursive: true });
|
||||
fs.writeFileSync(draftPath, JSON.stringify(data, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
/** Publish draft to live config atomically */
|
||||
export function publishDraft(): void {
|
||||
if (!fs.existsSync(draftPath)) return;
|
||||
const content = fs.readFileSync(draftPath, 'utf8');
|
||||
fs.mkdirSync(path.dirname(livePath), { recursive: true });
|
||||
fs.writeFileSync(livePath, content, 'utf8');
|
||||
}
|
||||
|
||||
/** Helper to get both draft and live (for debugging) */
|
||||
export function getLiveConfig(): Record<string, any> {
|
||||
if (fs.existsSync(livePath)) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(livePath, 'utf8'));
|
||||
} catch (e) {
|
||||
console.error('Failed to parse live JSON', e);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
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)'
|
||||
}
|
||||
}
|
||||
};
|
||||
23
tests/contactApi.test.ts
Normal file
23
tests/contactApi.test.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('Contact API Spamschutz & Validierung', () => {
|
||||
it('soll Honeypot-Anfragen lautlos abfangen', async () => {
|
||||
const hpPayload = {
|
||||
sender_name: 'Bot',
|
||||
sender_email: 'bot@spam.com',
|
||||
message_text: 'Spam text',
|
||||
privacy_consent: true,
|
||||
website_hp: 'http://spam-link.com'
|
||||
};
|
||||
|
||||
// Simulated check
|
||||
const isBot = Boolean(hpPayload.website_hp);
|
||||
expect(isBot).toBe(true);
|
||||
});
|
||||
|
||||
it('soll ungültige E-Mail-Adressen ablehnen', () => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
expect(emailRegex.test('invalid-email')).toBe(false);
|
||||
expect(emailRegex.test('max@beispiel.de')).toBe(true);
|
||||
});
|
||||
});
|
||||
39
tests/editorSections.test.ts
Normal file
39
tests/editorSections.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const DATA_DIR = path.join(process.cwd(), 'app', 'test_data_sections');
|
||||
const draftPath = path.join(DATA_DIR, 'site.config.draft.json');
|
||||
|
||||
|
||||
describe('Editor Sections API Test', () => {
|
||||
beforeEach(async () => {
|
||||
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||
const dummyConfig = {
|
||||
site_info: { title: 'Test Site', homepage_id: 'page_1' },
|
||||
pages: [
|
||||
{
|
||||
id: 'page_1',
|
||||
slug: '/',
|
||||
title: 'Home',
|
||||
sections: [{ id: 'sec_1', type: 'HeroSection', settings: { title: 'Old Hero' } }]
|
||||
}
|
||||
]
|
||||
};
|
||||
await fs.writeFile(draftPath, JSON.stringify(dummyConfig, null, 2), 'utf-8');
|
||||
});
|
||||
|
||||
it('soll Sektionen in site.config.draft.json erfolgreich überschreiben', async () => {
|
||||
const raw = await fs.readFile(draftPath, 'utf-8');
|
||||
const config = JSON.parse(raw);
|
||||
|
||||
config.pages[0].sections.push({ id: 'sec_2', type: 'BentoGrid', settings: {} });
|
||||
await fs.writeFile(draftPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
|
||||
const updatedRaw = await fs.readFile(draftPath, 'utf-8');
|
||||
const updatedConfig = JSON.parse(updatedRaw);
|
||||
|
||||
expect(updatedConfig.pages[0].sections).toHaveLength(2);
|
||||
expect(updatedConfig.pages[0].sections[1].type).toBe('BentoGrid');
|
||||
});
|
||||
});
|
||||
44
tests/editorSettings.test.ts
Normal file
44
tests/editorSettings.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const DATA_DIR = path.join(process.cwd(), 'app', 'test_data_settings');
|
||||
const draftPath = path.join(DATA_DIR, 'site.config.draft.json');
|
||||
|
||||
|
||||
describe('Editor Property API Test', () => {
|
||||
beforeEach(async () => {
|
||||
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||
const dummyConfig = {
|
||||
site_info: { title: 'Test Site', homepage_id: 'page_1' },
|
||||
pages: [
|
||||
{
|
||||
id: 'page_1',
|
||||
slug: '/',
|
||||
title: 'Home',
|
||||
sections: [
|
||||
{ id: 'sec_hero_123', type: 'HeroSection', settings: { title: 'Alter Titel', subtitle: 'Alt' } }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
await fs.writeFile(draftPath, JSON.stringify(dummyConfig, null, 2), 'utf-8');
|
||||
});
|
||||
|
||||
it('soll Sektions-Settings im Entwurf erfolgreich aktualisieren', async () => {
|
||||
const raw = await fs.readFile(draftPath, 'utf-8');
|
||||
const config = JSON.parse(raw);
|
||||
|
||||
const hero = config.pages[0].sections[0];
|
||||
hero.settings.title = 'Neuer Hero Titel';
|
||||
hero.settings.subtitle = 'Neuer Untertitel';
|
||||
|
||||
await fs.writeFile(draftPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
|
||||
const updatedRaw = await fs.readFile(draftPath, 'utf-8');
|
||||
const updatedConfig = JSON.parse(updatedRaw);
|
||||
|
||||
expect(updatedConfig.pages[0].sections[0].settings.title).toBe('Neuer Hero Titel');
|
||||
expect(updatedConfig.pages[0].sections[0].settings.subtitle).toBe('Neuer Untertitel');
|
||||
});
|
||||
});
|
||||
30
tests/fileGenerator.test.ts
Normal file
30
tests/fileGenerator.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { getDb } from '../src/db/index';
|
||||
import { generateDraftConfig, publishLiveConfig } from '../src/services/file-generator';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
describe('DB-Driven File Generation Service Test', () => {
|
||||
it('soll Entwurf aus DB lesen und site.config.draft.json generieren', async () => {
|
||||
const draftConfig = await generateDraftConfig();
|
||||
expect(draftConfig).toBeDefined();
|
||||
expect(draftConfig.pages).toBeDefined();
|
||||
expect(Array.isArray(draftConfig.pages)).toBe(true);
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||
const draftPath = path.join(DATA_DIR, 'site.config.draft.json');
|
||||
const exists = await fs.access(draftPath).then(() => true).catch(() => false);
|
||||
expect(exists).toBe(true);
|
||||
});
|
||||
|
||||
it('soll Live-Config aus DB freigeben und site.config.json atomar erstellen', async () => {
|
||||
const liveConfig = await publishLiveConfig();
|
||||
expect(liveConfig).toBeDefined();
|
||||
expect(liveConfig.pages).toBeDefined();
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||
const livePath = path.join(DATA_DIR, 'site.config.json');
|
||||
const exists = await fs.access(livePath).then(() => true).catch(() => false);
|
||||
expect(exists).toBe(true);
|
||||
});
|
||||
});
|
||||
18
tests/health.test.ts
Normal file
18
tests/health.test.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const DATA_DIR = path.join(process.cwd(), 'app', 'test_data_health');
|
||||
|
||||
describe('Healthcheck API Test', () => {
|
||||
it('soll Volume-Ordner erfolgreich erstellen und beschreiben können', async () => {
|
||||
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||
const testFile = path.join(DATA_DIR, '.vitest_health');
|
||||
|
||||
await fs.writeFile(testFile, 'test', 'utf-8');
|
||||
const content = await fs.readFile(testFile, 'utf-8');
|
||||
await fs.unlink(testFile);
|
||||
|
||||
expect(content).toBe('test');
|
||||
});
|
||||
});
|
||||
33
tests/siteSettings.test.ts
Normal file
33
tests/siteSettings.test.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const DATA_DIR = path.join(process.cwd(), 'app', 'test_data_site_settings');
|
||||
const configPath = path.join(DATA_DIR, 'site.config.json');
|
||||
|
||||
describe('Global Site Settings API Test', () => {
|
||||
beforeEach(async () => {
|
||||
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||
const dummyConfig = {
|
||||
site_info: { title: 'Alte Firma', phone: '0123456' },
|
||||
navigation: [{ label: 'Home', page_id: 'page_1' }]
|
||||
};
|
||||
await fs.writeFile(configPath, JSON.stringify(dummyConfig, null, 2), 'utf-8');
|
||||
});
|
||||
|
||||
it('soll globale Stammdaten in site.config.json aktualisieren', async () => {
|
||||
const raw = await fs.readFile(configPath, 'utf-8');
|
||||
const config = JSON.parse(raw);
|
||||
|
||||
config.site_info.title = 'N&D IT Solutions GmbH';
|
||||
config.site_info.phone = '+49 7473 123456';
|
||||
|
||||
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
|
||||
const updatedRaw = await fs.readFile(configPath, 'utf-8');
|
||||
const updatedConfig = JSON.parse(updatedRaw);
|
||||
|
||||
expect(updatedConfig.site_info.title).toBe('N&D IT Solutions GmbH');
|
||||
expect(updatedConfig.site_info.phone).toBe('+49 7473 123456');
|
||||
});
|
||||
});
|
||||
114
tests/waasEngine.test.ts
Normal file
114
tests/waasEngine.test.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
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');
|
||||
});
|
||||
|
||||
it('5. Should load device-specific configs for desktop, mobile, tablet with fallback', () => {
|
||||
fs.mkdirSync(testDataDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(testDataDir, 'site.mobile.config.json'), JSON.stringify({ siteName: 'Mobile Site' }), 'utf8');
|
||||
fs.writeFileSync(path.join(testDataDir, 'site.tablet.config.json'), JSON.stringify({ siteName: 'Tablet Site' }), 'utf8');
|
||||
fs.writeFileSync(path.join(testDataDir, 'site.desktop.config.json'), JSON.stringify({ siteName: 'Desktop Site' }), 'utf8');
|
||||
|
||||
const mobileConfig = loadWaasConfigs('Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)');
|
||||
expect(mobileConfig.siteConfig.siteName).toBe('Mobile Site');
|
||||
|
||||
const tabletConfig = loadWaasConfigs('Mozilla/5.0 (iPad; CPU OS 14_0 like Mac OS X)');
|
||||
expect(tabletConfig.siteConfig.siteName).toBe('Tablet Site');
|
||||
|
||||
const desktopConfig = loadWaasConfigs('Mozilla/5.0 (Windows NT 10.0; Win64; x64)');
|
||||
expect(desktopConfig.siteConfig.siteName).toBe('Desktop Site');
|
||||
});
|
||||
|
||||
it('6. Should correctly resolve Theme Builder template based on Display Conditions', async () => {
|
||||
const { resolveTemplate } = await import('../src/utils/conditionResolver');
|
||||
const templates = [
|
||||
{
|
||||
id: 'header-global',
|
||||
name: 'Global Header',
|
||||
type: 'header' as const,
|
||||
status: 'published' as const,
|
||||
priority: 1,
|
||||
conditions: [{ type: 'include' as const, scope: 'entire_site' as const }]
|
||||
},
|
||||
{
|
||||
id: 'header-custom-page',
|
||||
name: 'Custom Page Header',
|
||||
type: 'header' as const,
|
||||
status: 'published' as const,
|
||||
priority: 1,
|
||||
conditions: [{ type: 'include' as const, scope: 'singular' as const, entityId: 'landing_1' }]
|
||||
}
|
||||
];
|
||||
|
||||
// Global matching
|
||||
const matchGlobal = resolveTemplate(templates, 'header', { url: '/about', routeType: 'singular', entityId: 'about_page' });
|
||||
expect(matchGlobal?.id).toBe('header-global');
|
||||
|
||||
// Specific page matching
|
||||
const matchSpecific = resolveTemplate(templates, 'header', { url: '/landing', routeType: 'singular', entityId: 'landing_1' });
|
||||
expect(matchSpecific?.id).toBe('header-custom-page');
|
||||
});
|
||||
|
||||
it('7. Should correctly resolve dynamic data tags like {{site.siteName}}', async () => {
|
||||
const { resolveDynamicBinding } = await import('../src/utils/dynamicDataBinding');
|
||||
|
||||
const context = {
|
||||
site: { siteName: 'Test Agency Pro', contactEmail: 'info@test.de' },
|
||||
post: { title: 'Mein erster Post' }
|
||||
};
|
||||
|
||||
const resolvedTitle = resolveDynamicBinding('Willkommen bei {{site.siteName}}', context);
|
||||
expect(resolvedTitle).toBe('Willkommen bei Test Agency Pro');
|
||||
|
||||
const resolvedPost = resolveDynamicBinding('Artikel: {{post.title}}', context);
|
||||
expect(resolvedPost).toBe('Artikel: Mein erster Post');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user