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

36 KiB
Raw Permalink Blame History

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
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
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
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
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
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):

node -e "const b = require('bcryptjs'); b.hash('DEIN_PASSWORT', 12).then(h => console.log(h))"

Dann im SQL-Editor:

INSERT INTO admin_users (email, name, password_hash)
VALUES ('jonny@mbo-tech-it.de', 'Admin', '<HASH_AUS_OBIGEM_BEFEHL>');
  • Schritt 3.3: Commit
git commit --allow-empty -m "chore: DB-Migration CMS + Admin-User angelegt"

Task 4: npm-Pakete installieren

  • Schritt 4.1: Pakete installieren
npm install nodemailer bcryptjs
npm install -D @types/nodemailer @types/bcryptjs
  • Schritt 4.2: Überprüfen
npx tsc --noEmit

Erwartet: Keine neuen Fehler (nur ggf. bestehende, die mit fehlenden DB-Typen zusammenhängen).

  • Schritt 4.3: Commit
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:

      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
npx tsc --noEmit

Erwartet: Keine Fehler in lib/supabase.ts.

  • Schritt 5.3: Commit
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)
$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
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
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:

// 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
npx tsc --noEmit
  • Schritt 7.3: Commit
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:

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:

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
git add components/admin/HeroVerwaltung.tsx app/api/admin/hero/route.ts
git commit -m "feat: set MBO-Tech-IT defaults in hero verwaltung"

Datei: components/admin/AdminNav.tsx

  • Schritt 9.1: navLinks-Array erweitern

Den bestehenden navLinks-Array ersetzen:

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
npx tsc --noEmit
  • Schritt 9.3: Commit
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:

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

{/* 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):

{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
npx tsc --noEmit
  • Schritt 10.6: Commit
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):

interface AboutProps {
  eyebrowText?: string;
  absatz1?: string;
  absatz2?: string;
  stats?: { wert: string; label: string }[];
}
  • Schritt 11.2: Funktionssignatur anpassen
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:

<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):

{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
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):

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:

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):

{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
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
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
npx tsc --noEmit

Erwartet: Keine Fehler.

  • Schritt 13.3: Dev-Server starten und Seite prüfen
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

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
npx tsc --noEmit

Erwartet: 0 Fehler.

  • Schritt 14.2: Produktions-Build prüfen
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

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)