MBO-Tech-IT-Webseite/docs/superpowers/plans/2026-05-12-module-supabase-...

1101 lines
36 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Modul-Integration & Supabase-Anbindung — Implementierungsplan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Alle 6 Module vollständig einbinden — DB-Tabellen anlegen, CMS-Code integrieren, öffentliche Sektionen aus Supabase laden.
**Architecture:** Next.js 15 App Router + selbst-gehostetes Supabase (Docker VM). Öffentliche Seiten sind async Server Components; Client-Komponenten (Hero, Contact) nehmen Props entgegen. Alle Supabase-Aufrufe in `try/catch` mit hartkodiertem Fallback.
**Tech Stack:** Next.js 15, Supabase (PostgreSQL + Storage + Auth), Nodemailer, bcryptjs, `@supabase/ssr`
---
## Dateiübersicht
**Neue Dateien (Modul 06-CMS kopieren):**
```
app/api/admin/hero/route.ts
app/api/admin/hero/bild/route.ts
app/api/admin/hero/logo/route.ts
app/api/admin/hero/favicon/route.ts
app/api/admin/hero/badges/route.ts
app/api/admin/hero/badges/[id]/route.ts
app/api/admin/galerie/route.ts
app/api/admin/galerie/sort/route.ts
app/api/admin/ueber-uns/route.ts
app/api/admin/ueber-uns/upload/route.ts
app/api/admin/ueber-uns/stats/route.ts
app/api/admin/ueber-uns/stats/[id]/route.ts
app/api/admin/kontakt/route.ts
app/api/admin/kontakt/oeffnungszeiten/route.ts
app/api/admin/kontakt/oeffnungszeiten/[id]/route.ts
app/api/admin/kontakt/social/route.ts
app/api/admin/kontakt/social/[id]/route.ts
app/api/admin/passwort/route.ts
app/admin/hero/page.tsx
app/admin/galerie/page.tsx
app/admin/ueber-uns/page.tsx
app/admin/kontakt/page.tsx
app/admin/passwort/page.tsx
components/GalerieAnzeige.tsx
components/admin/HeroVerwaltung.tsx
components/admin/GalerieVerwaltung.tsx
components/admin/UeberUnsVerwaltung.tsx
components/admin/KontaktVerwaltung.tsx
components/admin/PasswortAendern.tsx
```
**Geänderte Dateien:**
```
lib/supabase.ts — CMS-Tabellentypen ergänzen
app/page.tsx — async, lädt aus Supabase
components/Hero.tsx — Props entgegennehmen
components/About.tsx — Props entgegennehmen
components/Contact.tsx — Props entgegennehmen
components/admin/AdminNav.tsx — CMS-Links ergänzen
app/api/admin/passwort/route.ts — 'admins' → 'admin_users'
```
**Bereits vorhanden (kein Handlungsbedarf):**
```
instrumentation.ts ✅ Email-Queue-Worker bereits konfiguriert
app/layout.tsx ✅ PageTracker bereits eingebunden
lib/mailer.ts, lib/email-queue.ts, lib/admin-auth.ts ✅
```
---
## Task 1: DB-Migrationen — Basis-Tabellen
**Auszuführen im Supabase SQL-Editor** (`https://supabase.mbo-tech-it.de`)
- [ ] **Schritt 1.1: email_queue anlegen**
```sql
CREATE TABLE IF NOT EXISTS email_queue (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
mail_from text NOT NULL,
mail_to text NOT NULL,
reply_to text,
subject text NOT NULL,
html text NOT NULL,
body_text text NOT NULL,
status text NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'sent', 'failed')),
retry_count int NOT NULL DEFAULT 0,
next_retry_at timestamptz NOT NULL DEFAULT now(),
error_last text,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_email_queue_pending
ON email_queue(status, next_retry_at) WHERE status = 'pending';
ALTER TABLE email_queue ENABLE ROW LEVEL SECURITY;
CREATE POLICY "service_role_email_queue" ON email_queue
USING (true) WITH CHECK (true);
```
- [ ] **Schritt 1.2: admin_users anlegen**
```sql
CREATE TABLE IF NOT EXISTS admin_users (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
email text UNIQUE NOT NULL,
name text,
password_hash text NOT NULL,
aktiv boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE admin_users ENABLE ROW LEVEL SECURITY;
CREATE POLICY "service_role_admin_users" ON admin_users
USING (true) WITH CHECK (true);
```
- [ ] **Schritt 1.3: anfragen anlegen**
```sql
CREATE TABLE IF NOT EXISTS anfragen (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL,
email text NOT NULL,
betreff text NOT NULL,
nachricht text,
status text NOT NULL DEFAULT 'offen',
admin_notizen text,
kunde_id uuid,
created_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE anfragen ENABLE ROW LEVEL SECURITY;
CREATE POLICY "service_role_anfragen" ON anfragen
USING (true) WITH CHECK (true);
CREATE POLICY "kunden_eigene_anfragen" ON anfragen
FOR SELECT USING (auth.uid() = kunde_id);
```
- [ ] **Schritt 1.4: Commit**
```bash
git commit --allow-empty -m "chore: DB-Migrationen 1-3 im Supabase SQL-Editor ausgeführt"
```
---
## Task 2: DB-Migrationen — Module 02 & 03
**Auszuführen im Supabase SQL-Editor**
- [ ] **Schritt 2.1: Modul 02 — Audit-Logs**
Inhalt von `modules/02-admin-auth/migrations/MIGRATIONS_AUDIT_LOGS.sql` ausführen (Tabelle `admin_audit_logs`).
- [ ] **Schritt 2.2: Modul 02 — Token-Blacklist**
Inhalt von `modules/02-admin-auth/migrations/MIGRATIONS_TOKEN_BLACKLIST.sql` ausführen (Tabellen `admin_session_blacklist`, `action_token_blacklist`).
- [ ] **Schritt 2.3: Modul 03 — Page Views**
Inhalt von `modules/03-analytics/migrations/MIGRATIONS_PAGE_VIEWS.sql` ausführen (Tabelle `page_views`).
- [ ] **Schritt 2.4: Modul 03 — Phone Clicks**
Inhalt von `modules/03-analytics/migrations/MIGRATIONS_PHONE_CLICKS.sql` ausführen (Tabelle `phone_clicks`).
- [ ] **Schritt 2.5: Commit**
```bash
git commit --allow-empty -m "chore: DB-Migrationen Modul 02+03 ausgeführt"
```
---
## Task 3: DB-Migration — Modul 06-CMS
**Auszuführen im Supabase SQL-Editor**
- [ ] **Schritt 3.1: CMS-Migration ausführen**
Vollständigen Inhalt von `modules/06-website-cms/migrations/MIGRATIONS_WEBSITE_CMS.sql` im SQL-Editor ausführen.
Erstellt diese Tabellen:
- `hero_content`, `hero_badges`
- `ueber_uns_content`, `ueber_uns_stats`
- `galerie_bilder`
- `kontakt_info`, `kontakt_oeffnungszeiten`, `kontakt_social`
- [ ] **Schritt 3.2: Ersten Admin-User anlegen**
> **Hinweis:** Dieser Schritt erfordert `bcryptjs` im node_modules. Task 4 (npm install) zuerst ausführen, dann hierher zurückkehren.
> Alternativ: Online-Bcrypt-Generator (z.B. https://bcrypt.online) mit Cost Factor 12 nutzen.
Bcrypt-Hash erzeugen (nach Task 4):
```bash
node -e "const b = require('bcryptjs'); b.hash('DEIN_PASSWORT', 12).then(h => console.log(h))"
```
Dann im SQL-Editor:
```sql
INSERT INTO admin_users (email, name, password_hash)
VALUES ('jonny@mbo-tech-it.de', 'Admin', '<HASH_AUS_OBIGEM_BEFEHL>');
```
- [ ] **Schritt 3.3: Commit**
```bash
git commit --allow-empty -m "chore: DB-Migration CMS + Admin-User angelegt"
```
---
## Task 4: npm-Pakete installieren
- [ ] **Schritt 4.1: Pakete installieren**
```bash
npm install nodemailer bcryptjs
npm install -D @types/nodemailer @types/bcryptjs
```
- [ ] **Schritt 4.2: Überprüfen**
```bash
npx tsc --noEmit
```
Erwartet: Keine neuen Fehler (nur ggf. bestehende, die mit fehlenden DB-Typen zusammenhängen).
- [ ] **Schritt 4.3: Commit**
```bash
git add package.json package-lock.json
git commit -m "chore: add nodemailer and bcryptjs dependencies"
```
---
## Task 5: `lib/supabase.ts` — CMS-Tabellentypen ergänzen
**Datei:** `lib/supabase.ts`
- [ ] **Schritt 5.1: CMS-Typen in `Database.public.Tables` einfügen**
Den bestehenden Abschluss-Block `};` des `Tables`-Objekts vor dem Schließen um folgende Typen erweitern:
```typescript
hero_content: {
Row: {
id: string;
site_name: string;
site_tagline: string;
logo_path: string | null;
favicon_path: string | null;
eyebrow_text: string;
headline1: string;
headline2: string;
subtext1: string;
subtext2: string;
cta1_text: string;
cta1_href: string;
cta2_text: string;
cta2_href: string;
bg_image_path: string | null;
updated_at: string;
};
Insert: {
site_name?: string;
site_tagline?: string;
logo_path?: string | null;
favicon_path?: string | null;
eyebrow_text?: string;
headline1?: string;
headline2?: string;
subtext1?: string;
subtext2?: string;
cta1_text?: string;
cta1_href?: string;
cta2_text?: string;
cta2_href?: string;
bg_image_path?: string | null;
updated_at?: string;
};
Update: {
site_name?: string;
site_tagline?: string;
logo_path?: string | null;
favicon_path?: string | null;
eyebrow_text?: string;
headline1?: string;
headline2?: string;
subtext1?: string;
subtext2?: string;
cta1_text?: string;
cta1_href?: string;
cta2_text?: string;
cta2_href?: string;
bg_image_path?: string | null;
updated_at?: string;
};
Relationships: [];
};
hero_badges: {
Row: { id: string; text: string; reihenfolge: number };
Insert: { text: string; reihenfolge?: number };
Update: { text?: string; reihenfolge?: number };
Relationships: [];
};
ueber_uns_content: {
Row: {
id: string;
eyebrow_text: string;
absatz1: string;
absatz2: string;
bild_url: string | null;
updated_at: string;
};
Insert: {
eyebrow_text?: string;
absatz1?: string;
absatz2?: string;
bild_url?: string | null;
updated_at?: string;
};
Update: {
eyebrow_text?: string;
absatz1?: string;
absatz2?: string;
bild_url?: string | null;
updated_at?: string;
};
Relationships: [];
};
ueber_uns_stats: {
Row: { id: string; wert: string; label: string; reihenfolge: number };
Insert: { wert: string; label: string; reihenfolge?: number };
Update: { wert?: string; label?: string; reihenfolge?: number };
Relationships: [];
};
galerie_bilder: {
Row: {
id: string;
storage_path: string;
alt_text: string;
reihenfolge: number;
};
Insert: { storage_path: string; alt_text?: string; reihenfolge?: number };
Update: { storage_path?: string; alt_text?: string; reihenfolge?: number };
Relationships: [];
};
kontakt_info: {
Row: {
id: string;
telefon: string;
email: string;
adresse_zeile1: string;
adresse_zeile2: string;
formular_empfaenger: string;
updated_at: string;
};
Insert: {
telefon?: string;
email?: string;
adresse_zeile1?: string;
adresse_zeile2?: string;
formular_empfaenger?: string;
updated_at?: string;
};
Update: {
telefon?: string;
email?: string;
adresse_zeile1?: string;
adresse_zeile2?: string;
formular_empfaenger?: string;
updated_at?: string;
};
Relationships: [];
};
kontakt_oeffnungszeiten: {
Row: { id: string; tag: string; von: string; bis: string; reihenfolge: number };
Insert: { tag: string; von: string; bis: string; reihenfolge?: number };
Update: { tag?: string; von?: string; bis?: string; reihenfolge?: number };
Relationships: [];
};
kontakt_social: {
Row: { id: string; platform: string; url: string; reihenfolge: number };
Insert: { platform: string; url: string; reihenfolge?: number };
Update: { platform?: string; url?: string; reihenfolge?: number };
Relationships: [];
};
```
- [ ] **Schritt 5.2: TypeScript-Check**
```bash
npx tsc --noEmit
```
Erwartet: Keine Fehler in `lib/supabase.ts`.
- [ ] **Schritt 5.3: Commit**
```bash
git add lib/supabase.ts
git commit -m "feat: add CMS table types to supabase.ts"
```
---
## Task 6: Modul 06-CMS Dateien kopieren
- [ ] **Schritt 6.1: Reguläre Dateien kopieren (PowerShell)**
```powershell
$src = "modules\06-website-cms\files"
$dst = "."
# API-Routen (ohne id/-Ordner)
Copy-Item "$src\app\api\admin\hero\route.ts" -Destination "app\api\admin\hero\" -Force
Copy-Item "$src\app\api\admin\hero\bild\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\hero\bild").FullName
Copy-Item "$src\app\api\admin\hero\logo\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\hero\logo").FullName
Copy-Item "$src\app\api\admin\hero\favicon\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\hero\favicon").FullName
Copy-Item "$src\app\api\admin\hero\badges\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\hero\badges").FullName
Copy-Item "$src\app\api\admin\galerie\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\galerie").FullName
Copy-Item "$src\app\api\admin\galerie\sort\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\galerie\sort").FullName
Copy-Item "$src\app\api\admin\ueber-uns\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\ueber-uns").FullName
Copy-Item "$src\app\api\admin\ueber-uns\upload\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\ueber-uns\upload").FullName
Copy-Item "$src\app\api\admin\ueber-uns\stats\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\ueber-uns\stats").FullName
Copy-Item "$src\app\api\admin\kontakt\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\kontakt").FullName
Copy-Item "$src\app\api\admin\kontakt\oeffnungszeiten\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\kontakt\oeffnungszeiten").FullName
Copy-Item "$src\app\api\admin\kontakt\social\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\kontakt\social").FullName
Copy-Item "$src\app\api\admin\passwort\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\passwort").FullName
# Dynamic-Route-Ordner [id] (Ordner heißt im Modul "id", muss "[id]" heißen)
Copy-Item "$src\app\api\admin\hero\badges\id\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\hero\badges\[id]").FullName
Copy-Item "$src\app\api\admin\ueber-uns\stats\id\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\ueber-uns\stats\[id]").FullName
Copy-Item "$src\app\api\admin\kontakt\oeffnungszeiten\id\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\kontakt\oeffnungszeiten\[id]").FullName
Copy-Item "$src\app\api\admin\kontakt\social\id\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\kontakt\social\[id]").FullName
# Admin-Seiten
Copy-Item "$src\app\admin\hero\page.tsx" -Destination (New-Item -ItemType Directory -Force "app\admin\hero").FullName
Copy-Item "$src\app\admin\galerie\page.tsx" -Destination (New-Item -ItemType Directory -Force "app\admin\galerie").FullName
Copy-Item "$src\app\admin\ueber-uns\page.tsx" -Destination (New-Item -ItemType Directory -Force "app\admin\ueber-uns").FullName
Copy-Item "$src\app\admin\kontakt\page.tsx" -Destination (New-Item -ItemType Directory -Force "app\admin\kontakt").FullName
Copy-Item "$src\app\admin\passwort\page.tsx" -Destination (New-Item -ItemType Directory -Force "app\admin\passwort").FullName
# Komponenten
Copy-Item "$src\components\GalerieAnzeige.tsx" -Destination "components\" -Force
Copy-Item "$src\components\admin\HeroVerwaltung.tsx" -Destination "components\admin\" -Force
Copy-Item "$src\components\admin\GalerieVerwaltung.tsx" -Destination "components\admin\" -Force
Copy-Item "$src\components\admin\UeberUnsVerwaltung.tsx" -Destination "components\admin\" -Force
Copy-Item "$src\components\admin\KontaktVerwaltung.tsx" -Destination "components\admin\" -Force
Copy-Item "$src\components\admin\PasswortAendern.tsx" -Destination "components\admin\" -Force
```
- [ ] **Schritt 6.2: Verify**
```bash
ls app/api/admin/hero/
ls app/api/admin/hero/badges/
```
Erwartet: `route.ts`, `bild/`, `logo/`, `favicon/`, `badges/` (mit `route.ts` und `[id]/route.ts`)
- [ ] **Schritt 6.3: Commit**
```bash
git add app/api/admin/ app/admin/hero/ app/admin/galerie/ app/admin/ueber-uns/ app/admin/kontakt/ app/admin/passwort/ components/GalerieAnzeige.tsx components/admin/HeroVerwaltung.tsx components/admin/GalerieVerwaltung.tsx components/admin/UeberUnsVerwaltung.tsx components/admin/KontaktVerwaltung.tsx components/admin/PasswortAendern.tsx
git commit -m "feat: add website-cms module files"
```
---
## Task 7: `app/api/admin/passwort/route.ts` anpassen
Die kopierte Datei referenziert `db.from('admins')` — muss `admin_users` sein.
**Datei:** `app/api/admin/passwort/route.ts`
- [ ] **Schritt 7.1: Tabellenname korrigieren**
Beide Vorkommen von `'admins'` durch `'admin_users'` ersetzen:
```typescript
// Zeile ~19: vorher
const { data: admin } = await db.from('admins').select('password_hash').eq('id', session.id).single()
// nachher
const { data: admin } = await db.from('admin_users').select('password_hash').eq('id', session.id).single()
// Zeile ~25: vorher
const { error } = await db.from('admins').update({ password_hash: hash }).eq('id', session.id)
// nachher
const { error } = await db.from('admin_users').update({ password_hash: hash }).eq('id', session.id)
```
- [ ] **Schritt 7.2: TypeScript-Check**
```bash
npx tsc --noEmit
```
- [ ] **Schritt 7.3: Commit**
```bash
git add app/api/admin/passwort/route.ts
git commit -m "fix: use admin_users table in passwort route"
```
---
## Task 8: `components/admin/HeroVerwaltung.tsx` — Standardwerte anpassen
- [ ] **Schritt 8.1: DEFAULTS im Code anpassen**
In `components/admin/HeroVerwaltung.tsx` den `DEFAULTS`-Block suchen (ca. Zeile 33) und ersetzen:
```typescript
const DEFAULTS: HeroContent = {
site_name: 'MBO-Tech-IT',
site_tagline: 'IT-Service Crailsheim',
eyebrow_text: 'Digital Denken. Lokal Handeln.',
headline1: 'Ihre IT-Infrastruktur.',
headline2: 'Professionell. Zuverlässig.',
subtext1: 'Ihr IT-Dienstleister in Crailsheim — egal ob zu Hause oder im Büro.',
subtext2: '',
cta1_text: 'Jetzt anfragen', cta1_href: '#contact',
cta2_text: 'Unsere Leistungen', cta2_href: '#services',
bg_image_path: null,
}
```
- [ ] **Schritt 8.2: Gleiche Defaults in API-Route setzen**
In `app/api/admin/hero/route.ts` den `fields`-Block im PATCH-Handler anpassen:
```typescript
const fields = {
site_name: body.site_name ?? 'MBO-Tech-IT',
site_tagline: body.site_tagline ?? 'IT-Service Crailsheim',
eyebrow_text: body.eyebrow_text ?? 'Digital Denken. Lokal Handeln.',
headline1: body.headline1 ?? 'Ihre IT-Infrastruktur.',
headline2: body.headline2 ?? 'Professionell. Zuverlässig.',
subtext1: body.subtext1 ?? 'Ihr IT-Dienstleister in Crailsheim',
subtext2: body.subtext2 ?? '',
cta1_text: body.cta1_text ?? 'Jetzt anfragen',
cta1_href: body.cta1_href ?? '#contact',
cta2_text: body.cta2_text ?? 'Unsere Leistungen',
cta2_href: body.cta2_href ?? '#services',
updated_at: new Date().toISOString(),
}
```
- [ ] **Schritt 8.3: Commit**
```bash
git add components/admin/HeroVerwaltung.tsx app/api/admin/hero/route.ts
git commit -m "feat: set MBO-Tech-IT defaults in hero verwaltung"
```
---
## Task 9: `components/admin/AdminNav.tsx` — CMS-Links ergänzen
**Datei:** `components/admin/AdminNav.tsx`
- [ ] **Schritt 9.1: `navLinks`-Array erweitern**
Den bestehenden `navLinks`-Array ersetzen:
```typescript
const navLinks = [
{ href: "/admin/hero", label: "Hero" },
{ href: "/admin/ueber-uns", label: "Über uns" },
{ href: "/admin/galerie", label: "Galerie" },
{ href: "/admin/kontakt", label: "Kontakt" },
{ href: "/admin/passwort", label: "Passwort" },
{ href: "/admin/analytics", label: "Analytics" },
{ href: "/admin/statistik", label: "Statistik" },
{ href: "/admin/audit-logs", label: "Audit-Logs" },
];
```
- [ ] **Schritt 9.2: TypeScript-Check**
```bash
npx tsc --noEmit
```
- [ ] **Schritt 9.3: Commit**
```bash
git add components/admin/AdminNav.tsx
git commit -m "feat: add CMS links to admin navigation"
```
---
## Task 10: `components/Hero.tsx` — Props entgegennehmen
Die Komponente bleibt `"use client"` (wegen `useTypewriter`), nimmt aber Inhalte als Props.
**Datei:** `components/Hero.tsx`
- [ ] **Schritt 10.1: Props-Interface + Defaults hinzufügen**
Am Anfang der Datei nach dem Import-Block einfügen:
```typescript
interface HeroProps {
eyebrowText?: string;
headline1?: string;
subtext1?: string;
subtext2?: string;
cta1Text?: string;
cta1Href?: string;
cta2Text?: string;
cta2Href?: string;
bgImagePath?: string | null;
badges?: string[];
}
```
- [ ] **Schritt 10.2: Funktionssignatur anpassen**
```typescript
export default function Hero({
eyebrowText = "Digital Denken. Lokal Handeln.",
headline1 = "Ihre IT-Infrastruktur.",
subtext1 = "Ihr IT-Dienstleister in Crailsheim — egal ob zu Hause oder im Büro. Von Hard- & Software über Netzwerk & WLAN bis hin zu Webseiten & Webanwendungen: Wir lösen Ihre IT-Probleme persönlich und zum fairen Preis.",
subtext2 = "",
cta1Text = "Jetzt anfragen",
cta1Href = "#contact",
cta2Text = "Unsere Leistungen",
cta2Href = "#services",
bgImagePath = null,
badges = ["Docker", "Kubernetes", "Proxmox", "Hetzner Cloud", "Linux"],
}: HeroProps = {}) {
```
- [ ] **Schritt 10.3: JSX anpassen — Badge, Headline, Subtext, Buttons**
Im JSX-Block die hardkodierten Werte durch Props ersetzen:
```tsx
{/* Tagline badge */}
<div className="inline-flex items-center gap-2 px-5 py-2 rounded-full border border-orange-500/40 bg-orange-500/10 text-orange-400 text-xs font-black tracking-[0.3em] uppercase mb-8">
<span className="w-1.5 h-1.5 bg-orange-400 rounded-full animate-pulse" />
{eyebrowText}
</div>
{/* Headline */}
<h1 className="text-5xl sm:text-6xl lg:text-7xl font-black text-slate-900 dark:text-white leading-tight mb-6 tracking-tight">
{headline1}{" "}
<span className="text-gradient">
{typed}
<span className="animate-pulse">|</span>
</span>
</h1>
{/* Subheadline */}
{subtext1 && (
<p className="text-xl sm:text-2xl text-slate-600 dark:text-slate-400 max-w-3xl mx-auto mb-4 leading-relaxed">
{subtext1}
</p>
)}
{subtext2 && (
<p className="text-xl sm:text-2xl text-slate-600 dark:text-slate-400 max-w-3xl mx-auto mb-4 leading-relaxed">
{subtext2}
</p>
)}
{/* Tech pills (aus hero_badges) */}
<div className="flex flex-wrap items-center justify-center gap-3 mb-10">
{badges.map((badge) => (
<span
key={badge}
className="px-3 py-1 rounded-md bg-slate-100 dark:bg-gray-900 border border-slate-200 dark:border-gray-700 text-slate-700 dark:text-slate-300 text-sm font-mono"
>
{badge}
</span>
))}
</div>
{/* CTAs */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<a href={cta1Href} className="btn-primary w-full sm:w-auto px-8 py-4 text-lg">
{cta1Text}
</a>
<a href={cta2Href} className="btn-secondary w-full sm:w-auto px-8 py-4 text-lg">
{cta2Text}
</a>
</div>
```
- [ ] **Schritt 10.4: Hintergrundbild optional**
Das Background-`<div>` mit bedingtem Bild ergänzen (falls `bgImagePath` gesetzt):
```tsx
{bgImagePath && (
<div
className="absolute inset-0 bg-cover bg-center opacity-20"
style={{ backgroundImage: `url(${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/object/public/hero-bilder/${bgImagePath})` }}
/>
)}
```
- [ ] **Schritt 10.5: TypeScript-Check**
```bash
npx tsc --noEmit
```
- [ ] **Schritt 10.6: Commit**
```bash
git add components/Hero.tsx
git commit -m "feat: hero component accepts cms props"
```
---
## Task 11: `components/About.tsx` — Props entgegennehmen
**Datei:** `components/About.tsx`
- [ ] **Schritt 11.1: Props-Interface hinzufügen**
Am Dateianfang (vor dem `highlights`-Array):
```typescript
interface AboutProps {
eyebrowText?: string;
absatz1?: string;
absatz2?: string;
stats?: { wert: string; label: string }[];
}
```
- [ ] **Schritt 11.2: Funktionssignatur anpassen**
```typescript
export default function About({
eyebrowText = "Über uns",
absatz1 = "MBO-Tech-IT steht für über 30 Jahre IT-Erfahrung — angefangen bei der einfachen Hardware-Wartung, über den Aufbau großer Client-Server-Netzwerke, bis hin zu mehr als 20 Jahren Spezialisierung in der IT-Security.",
absatz2 = "Heute liegt der Fokus auf modernen Container-Technologien, Cloud-nativer Infrastruktur und Virtualisierung. Dieses breite Fundament ermöglicht es, Lösungen zu entwickeln, die nicht nur funktionieren — sondern auch sicher und langfristig tragfähig sind.",
stats,
}: AboutProps = {}) {
```
- [ ] **Schritt 11.3: JSX anpassen**
Die hartkodierten Texte im JSX durch Props ersetzen:
```tsx
<span className="text-orange-400 font-mono text-xs font-bold tracking-[0.25em] uppercase">
{eyebrowText}
</span>
<p className="text-slate-600 dark:text-slate-400 text-lg leading-relaxed mb-6">
{absatz1}
</p>
<p className="text-slate-600 dark:text-slate-400 text-lg leading-relaxed mb-8">
{absatz2}
</p>
```
Falls `stats` befüllt ist, die Statistiken rendern (als Ergänzung zu den bestehenden `highlights`):
```tsx
{stats && stats.length > 0 && (
<div className="flex gap-8 mt-4">
{stats.map((s) => (
<div key={s.label}>
<div className="text-3xl font-black text-orange-400">{s.wert}</div>
<div className="text-sm text-slate-500 dark:text-slate-400">{s.label}</div>
</div>
))}
</div>
)}
```
- [ ] **Schritt 11.4: Commit**
```bash
git add components/About.tsx
git commit -m "feat: about component accepts cms props"
```
---
## Task 12: `components/Contact.tsx` — Props entgegennehmen
**Datei:** `components/Contact.tsx`
- [ ] **Schritt 12.1: Props-Interface hinzufügen**
Am Dateianfang (nach dem `"use client"` Directive):
```typescript
interface OeffnungsZeit { tag: string; von: string; bis: string }
interface ContactProps {
telefon?: string;
email?: string;
adresseZeile1?: string;
adresseZeile2?: string;
oeffnungszeiten?: OeffnungsZeit[];
}
```
- [ ] **Schritt 12.2: Datei-level `contactItems`-Array entfernen und Funktionssignatur anpassen**
Die bestehende `const contactItems = [...]` auf Datei-Ebene (Zeilen 337) vollständig entfernen. Dann die Funktionssignatur ersetzen und `contactItems` in den Funktionskörper verschieben:
```typescript
export default function Contact({
telefon = "+49 171 9345193",
email = "kontakt@mbo-tech-it.de",
adresseZeile1 = "Mörikestr. 2",
adresseZeile2 = "74564 Crailsheim",
oeffnungszeiten,
}: ContactProps = {}) {
const contactItems = [
{
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>,
label: "Telefon",
value: telefon,
href: `tel:${telefon.replace(/\s/g, "")}`,
},
{
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>,
label: "E-Mail",
value: email,
href: `mailto:${email}`,
},
{
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>,
label: "Standort",
value: `${adresseZeile1}, ${adresseZeile2}`,
href: `https://maps.google.com/?q=${encodeURIComponent(adresseZeile1 + " " + adresseZeile2)}`,
},
]
```
- [ ] **Schritt 12.3: Öffnungszeiten-Block im JSX ergänzen**
Unterhalb der `contactItems`-Liste im JSX (nach der letzten `contactItems.map`-Ausgabe):
```tsx
{oeffnungszeiten && oeffnungszeiten.length > 0 && (
<div className="mt-6 pt-6 border-t border-slate-200 dark:border-slate-700">
<h4 className="text-sm font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider mb-3">
Öffnungszeiten
</h4>
<ul className="space-y-1">
{oeffnungszeiten.map((oz) => (
<li key={oz.tag} className="flex justify-between text-sm text-slate-600 dark:text-slate-400">
<span>{oz.tag}</span>
<span>{oz.von} {oz.bis}</span>
</li>
))}
</ul>
</div>
)}
```
- [ ] **Schritt 12.4: Commit**
```bash
git add components/Contact.tsx
git commit -m "feat: contact component accepts cms props"
```
---
## Task 13: `app/page.tsx` dynamisieren
**Datei:** `app/page.tsx`
- [ ] **Schritt 13.1: Datei vollständig ersetzen**
```typescript
import { createServiceClient } from "@/lib/supabase";
import Header from "@/components/Header";
import Hero from "@/components/Hero";
import TaglineBanner from "@/components/TaglineBanner";
import StatsBar from "@/components/StatsBar";
import Services from "@/components/Services";
import Technologies from "@/components/Technologies";
import DataSovereignty from "@/components/DataSovereignty";
import PaperlessSection from "@/components/PaperlessSection";
import About from "@/components/About";
import Contact from "@/components/Contact";
import Footer from "@/components/Footer";
import { GalerieAnzeige } from "@/components/GalerieAnzeige";
export const dynamic = "force-dynamic";
export default async function Home() {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL ?? "";
let hero = null,
badges: { text: string }[] = [],
ueberUns = null,
stats: { wert: string; label: string }[] = [],
galerie: { id: string; storage_path: string; alt_text: string }[] = [],
kontakt = null,
oeffnung: { tag: string; von: string; bis: string }[] = [];
try {
const db = createServiceClient();
const [
heroResult,
badgesResult,
ueberUnsResult,
statsResult,
galerieResult,
kontaktResult,
oeffnungResult,
] = await Promise.allSettled([
db.from("hero_content").select("*").limit(1).single(),
db.from("hero_badges").select("*").order("reihenfolge"),
db.from("ueber_uns_content").select("*").limit(1).single(),
db.from("ueber_uns_stats").select("*").order("reihenfolge"),
db.from("galerie_bilder").select("*").order("reihenfolge"),
db.from("kontakt_info").select("*").limit(1).single(),
db.from("kontakt_oeffnungszeiten").select("*").order("reihenfolge"),
]);
hero = heroResult.status === "fulfilled" ? heroResult.value.data : null;
badges = badgesResult.status === "fulfilled" ? (badgesResult.value.data ?? []) : [];
ueberUns = ueberUnsResult.status === "fulfilled" ? ueberUnsResult.value.data : null;
stats = statsResult.status === "fulfilled" ? (statsResult.value.data ?? []) : [];
galerie = galerieResult.status === "fulfilled" ? (galerieResult.value.data ?? []) : [];
kontakt = kontaktResult.status === "fulfilled" ? kontaktResult.value.data : null;
oeffnung = oeffnungResult.status === "fulfilled" ? (oeffnungResult.value.data ?? []) : [];
} catch {
// Supabase nicht konfiguriert — Fallback-Werte greifen
}
const galerieBilder = galerie.map((b) => ({
id: b.id,
src: `${supabaseUrl}/storage/v1/object/public/galerie-bilder/${b.storage_path}`,
alt: b.alt_text,
}));
return (
<main className="min-h-screen bg-[#f0f4f8] dark:bg-[#18212f]">
<Header />
<Hero
eyebrowText={hero?.eyebrow_text}
headline1={hero?.headline1}
subtext1={hero?.subtext1}
subtext2={hero?.subtext2}
cta1Text={hero?.cta1_text}
cta1Href={hero?.cta1_href}
cta2Text={hero?.cta2_text}
cta2Href={hero?.cta2_href}
bgImagePath={hero?.bg_image_path}
badges={badges.length > 0 ? badges.map((b) => b.text) : undefined}
/>
<TaglineBanner />
<StatsBar />
<Services />
<TaglineBanner />
<PaperlessSection />
<DataSovereignty />
<TaglineBanner />
<Technologies />
<About
eyebrowText={ueberUns?.eyebrow_text}
absatz1={ueberUns?.absatz1}
absatz2={ueberUns?.absatz2}
stats={stats.length > 0 ? stats : undefined}
/>
{galerieBilder.length > 0 && (
<>
<TaglineBanner />
<section className="py-16 px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto">
<GalerieAnzeige bilder={galerieBilder} />
</div>
</section>
</>
)}
<TaglineBanner />
<Contact
telefon={kontakt?.telefon}
email={kontakt?.email}
adresseZeile1={kontakt?.adresse_zeile1}
adresseZeile2={kontakt?.adresse_zeile2}
oeffnungszeiten={oeffnung.length > 0 ? oeffnung : undefined}
/>
<Footer />
</main>
);
}
```
- [ ] **Schritt 13.2: TypeScript-Check**
```bash
npx tsc --noEmit
```
Erwartet: Keine Fehler.
- [ ] **Schritt 13.3: Dev-Server starten und Seite prüfen**
```bash
npm run dev
```
Prüfen unter `http://localhost:3000`:
- Seite lädt ohne Fehler (Fallback-Werte werden gezeigt solange DB-Tabellen leer sind)
- Kein Absturz, kein 500-Error in der Konsole
- [ ] **Schritt 13.4: Commit**
```bash
git add app/page.tsx
git commit -m "feat: load homepage sections from supabase cms"
```
---
## Task 14: Build-Check & Abschluss
- [ ] **Schritt 14.1: TypeScript vollständig prüfen**
```bash
npx tsc --noEmit
```
Erwartet: 0 Fehler.
- [ ] **Schritt 14.2: Produktions-Build prüfen**
```bash
npm run build
```
Erwartet: Kein Build-Fehler. Nur Warnungen zu `force-dynamic` sind akzeptabel.
- [ ] **Schritt 14.3: Admin-Bereich testen**
Dev-Server starten und manuell testen:
1. `http://localhost:3000/admin/login` → Login mit dem angelegten Admin-User
2. `/admin/hero` → Texte anpassen, speichern, Startseite neu laden → Änderung sichtbar
3. `/admin/galerie` → Bild hochladen (Storage Bucket muss existieren)
4. `/admin/ueber-uns` → Text + Statistik speichern
5. `/admin/kontakt` → Öffnungszeiten + Kontaktdaten speichern
6. `/admin/passwort` → Passwort ändern, neu einloggen
- [ ] **Schritt 14.4: Supabase-Infrastructure (manuell im Dashboard)**
Im Supabase-Dashboard `https://supabase.mbo-tech-it.de`:
**Storage → New Bucket** (alle vier anlegen, Public = ✓):
- `hero-bilder`
- `site-assets`
- `ueber-uns-bilder`
- `galerie-bilder`
**Authentication → URL Configuration:**
- Site URL: `https://mbo-tech-it.de`
- Redirect URLs: `https://mbo-tech-it.de/auth/callback`
- Email Confirmations: aktivieren
- [ ] **Schritt 14.5: git push**
```bash
git push
```
---
## Abhängigkeiten zwischen Tasks
```
Task 1 (email_queue, admin_users, anfragen)
└── Task 2 (audit_logs, token_blacklist, page_views, phone_clicks)
└── Task 3 (CMS + Admin-User)
└── Task 4 (npm install)
└── Task 5 (supabase.ts Typen)
└── Task 6 (CMS-Dateien kopieren)
├── Task 7 (passwort route fix)
├── Task 8 (Hero-Defaults)
└── Task 9 (AdminNav)
└── Task 10 (Hero.tsx props)
└── Task 11 (About.tsx props)
└── Task 12 (Contact.tsx props)
└── Task 13 (page.tsx dynamisieren) ← benötigt Tasks 10-12
└── Task 14 (Build-Check)
```