diff --git a/docs/superpowers/plans/2026-05-12-module-supabase-integration.md b/docs/superpowers/plans/2026-05-12-module-supabase-integration.md new file mode 100644 index 0000000..ccbe73f --- /dev/null +++ b/docs/superpowers/plans/2026-05-12-module-supabase-integration.md @@ -0,0 +1,1100 @@ +# 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', ''); +``` + +- [ ] **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 */} +
+ + {eyebrowText} +
+ +{/* Headline */} +

+ {headline1}{" "} + + {typed} + | + +

+ +{/* Subheadline */} +{subtext1 && ( +

+ {subtext1} +

+)} +{subtext2 && ( +

+ {subtext2} +

+)} + +{/* Tech pills (aus hero_badges) */} +
+ {badges.map((badge) => ( + + {badge} + + ))} +
+ +{/* CTAs */} +
+ + {cta1Text} + + + {cta2Text} + +
+``` + +- [ ] **Schritt 10.4: Hintergrundbild optional** + +Das Background-`
` mit bedingtem Bild ergänzen (falls `bgImagePath` gesetzt): + +```tsx +{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 + + {eyebrowText} + + +

+ {absatz1} +

+

+ {absatz2} +

+``` + +Falls `stats` befüllt ist, die Statistiken rendern (als Ergänzung zu den bestehenden `highlights`): + +```tsx +{stats && stats.length > 0 && ( +
+ {stats.map((s) => ( +
+
{s.wert}
+
{s.label}
+
+ ))} +
+)} +``` + +- [ ] **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 3–37) 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: + + , + label: "Telefon", + value: telefon, + href: `tel:${telefon.replace(/\s/g, "")}`, + }, + { + icon: + + , + label: "E-Mail", + value: email, + href: `mailto:${email}`, + }, + { + icon: + + + , + 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 && ( +
+

+ Öffnungszeiten +

+
    + {oeffnungszeiten.map((oz) => ( +
  • + {oz.tag} + {oz.von} – {oz.bis} +
  • + ))} +
+
+)} +``` + +- [ ] **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 ( +
+
+ 0 ? badges.map((b) => b.text) : undefined} + /> + + + + + + + + + 0 ? stats : undefined} + /> + {galerieBilder.length > 0 && ( + <> + +
+
+ +
+
+ + )} + + 0 ? oeffnung : undefined} + /> +
+ ); +} +``` + +- [ ] **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) +```