diff --git a/docs/superpowers/plans/2026-07-21-familyguard-flyer-download.md b/docs/superpowers/plans/2026-07-21-familyguard-flyer-download.md new file mode 100644 index 0000000..09ba6ab --- /dev/null +++ b/docs/superpowers/plans/2026-07-21-familyguard-flyer-download.md @@ -0,0 +1,632 @@ +# FamilyGuard Flyer-Download Implementation Plan + +> **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:** Der FamilyGuard-Flyer wird über ein E-Mail-Gate herunterladbar: Interessent gibt E-Mail-Adresse ein (mit DSGVO-Einwilligung), bekommt sofort einen Download-Button + eine Bestätigungsmail mit Link, und Jonny (jonny@mbo-tech-it.de) wird per Mail benachrichtigt. + +**Architektur:** Neue API-Route `POST /api/familyguard-flyer` validiert Eingabe, speichert den Lead in einer neuen Supabase-Tabelle `flyer_downloads`, verschickt zwei Mails über die bestehende `lib/mailer.ts`-Infrastruktur (SMTP mit Queue-Fallback) und liefert die Download-URL zurück. Eine neue Client-Komponente `FlyerDownloadForm` kapselt Formular + Erfolgs-Zustand und wird einmalig auf der FamilyGuard-Seite eingebunden; die Homepage-Kachel verlinkt per Anchor dorthin. + +**Tech Stack:** Next.js 15 App Router, TypeScript (`strict: true`), Supabase (`@supabase/supabase-js`), Nodemailer (bestehend in `lib/mailer.ts`), Tailwind CSS. + +## Global Constraints + +- Kein Test-Framework im Projekt (kein Jest/Vitest, keine `*.test.ts`-Dateien existieren) — Verifikation erfolgt manuell über `npx tsc --noEmit`, `npm run dev` und `curl`, wie es auch bei allen bestehenden API-Routen (z. B. `app/api/contact/route.ts`) gehandhabt wurde. +- `@/*` ist auf das Root-Verzeichnis aliased. +- `JSX.Element` nicht verwenden — `ReactElement` aus `react` importieren, falls ein Rückgabetyp explizit gebraucht wird. +- Neue Server-seitige E-Mail-Funktionen folgen exakt dem bestehenden Muster in `lib/mailer.ts` (`sendWithFallback`-Helper, gleiches HTML-Layout/Branding). +- Die Migration für `flyer_downloads` wird **nicht** als Datei unter `modules/` abgelegt (dieser Ordner ist Jonnys projektübergreifender Wiederverwendungs-Katalog für generische Module) und auch nicht als SQL-Datei im Repo getrackt (kein anderer Content-Table hat das) — das SQL wird in Task 2 dokumentiert und von Jonny manuell im Supabase SQL-Editor ausgeführt. +- Git-Commits nach jedem Task, wie in diesem Projekt bisher gehandhabt (kein `--no-verify`, keine `-i`-Flags). + +--- + +### Task 1: Flyer-PDF nach `public/downloads/` verschieben + +**Files:** +- Move: `docs/MBO_FamilyGuard_Flyer_01.pdf` → `public/downloads/MBO_FamilyGuard_Flyer_01.pdf` + +**Interfaces:** +- Produziert: Statische URL `/downloads/MBO_FamilyGuard_Flyer_01.pdf`, die von Task 4 (API-Response) und Task 5 (Download-Button) referenziert wird. + +- [ ] **Step 1: Ordner anlegen und Datei verschieben** + +```bash +mkdir -p public/downloads +git mv docs/MBO_FamilyGuard_Flyer_01.pdf public/downloads/MBO_FamilyGuard_Flyer_01.pdf +``` + +- [ ] **Step 2: Verifizieren, dass die Datei unter der neuen URL ausgeliefert wird** + +```bash +npm run dev +``` + +Dann im Browser (oder mit curl in einem zweiten Terminal) prüfen: + +```bash +curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/downloads/MBO_FamilyGuard_Flyer_01.pdf +``` + +Erwartet: `200` + +- [ ] **Step 3: Commit** + +```bash +git add public/downloads/MBO_FamilyGuard_Flyer_01.pdf docs/MBO_FamilyGuard_Flyer_01.pdf +git commit -m "feat: move FamilyGuard flyer PDF to public/downloads for direct serving" +``` + +--- + +### Task 2: Supabase-Tabelle `flyer_downloads` + Typdefinition + +**Files:** +- Modify: `lib/supabase.ts` (Database-Typ ergänzen, nach dem `kontakt_social`-Block, vor der schließenden `};` der `Tables`) + +**Interfaces:** +- Produziert: Tabellentyp `flyer_downloads` mit `Row`/`Insert`/`Update`, den Task 4 (API-Route) für `db.from("flyer_downloads").insert(...)` benötigt. + +- [ ] **Step 1: SQL manuell im Supabase SQL-Editor ausführen** + +Dieser Schritt kann nicht automatisiert werden (lokale `.env.local` enthält nur Platzhalter-Credentials, kein echter Zugriff auf die Produktions-Supabase-Instanz). Jonny führt folgendes SQL in seiner Supabase-Instanz aus: + +```sql +CREATE TABLE IF NOT EXISTS flyer_downloads ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + email TEXT NOT NULL, + flyer TEXT NOT NULL DEFAULT 'familyguard', + dsgvo_einwilligung BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS idx_flyer_downloads_email ON flyer_downloads(email); +CREATE INDEX IF NOT EXISTS idx_flyer_downloads_created_at ON flyer_downloads(created_at DESC); + +ALTER TABLE flyer_downloads DISABLE ROW LEVEL SECURITY; +``` + +Verifikation (im SQL-Editor): + +```sql +SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'flyer_downloads'; +``` + +Erwartet: 5 Zeilen (`id`, `email`, `flyer`, `dsgvo_einwilligung`, `created_at`). + +- [ ] **Step 2: Datenbank-Typ in `lib/supabase.ts` ergänzen** + +In `lib/supabase.ts` direkt vor der schließenden `};` des `Tables`-Objekts (nach dem `kontakt_social`-Eintrag, aktuell endet dieser Block bei `Relationships: [];\n };`) folgenden Eintrag einfügen: + +```ts + flyer_downloads: { + Row: { + id: number; + email: string; + flyer: string; + dsgvo_einwilligung: boolean; + created_at: string; + }; + Insert: { + id?: number; + email: string; + flyer?: string; + dsgvo_einwilligung: boolean; + created_at?: string; + }; + Update: { + email?: string; + flyer?: string; + dsgvo_einwilligung?: boolean; + }; + Relationships: []; + }; +``` + +- [ ] **Step 3: TypeScript-Check** + +```bash +npx tsc --noEmit +``` + +Erwartet: keine Fehler. + +- [ ] **Step 4: Commit** + +```bash +git add lib/supabase.ts +git commit -m "feat: add flyer_downloads table type to Supabase schema" +``` + +--- + +### Task 3: Mailer-Funktionen für Flyer-Benachrichtigung und Flyer-Link + +**Files:** +- Modify: `lib/mailer.ts` (zwei neue Exporte am Ende der Datei ergänzen, nach `sendeKontaktEmail`) + +**Interfaces:** +- Konsumiert: `sendWithFallback` (bereits in `lib/mailer.ts` definiert, Zeile 40). +- Produziert: `sendeFlyerBenachrichtigung(data: { email: string }): Promise<{ sent: boolean; queued: boolean }>` und `sendeFlyerLink(data: { email: string }): Promise`, die Task 4 (API-Route) importiert. + +- [ ] **Step 1: Funktionen in `lib/mailer.ts` ergänzen** + +Am Ende der Datei (nach der schließenden `}` von `sendeKontaktEmail`) einfügen: + +```ts +export async function sendeFlyerBenachrichtigung( + data: { email: string } +): Promise<{ sent: boolean; queued: boolean }> { + const html = ` + + + + +
+

MBO Tech IT

+

FamilyGuard Flyer-Download

+
+
+

Neuer Flyer-Download

+

+ Ein Interessent hat den MBO FamilyGuard Flyer angefordert. +

+ + +
E-Mail${data.email}
+
+
+

MBO Tech IT · ${process.env.APP_URL ?? "https://mbo-tech-it.de"}

+
+ +`; + + return sendWithFallback( + { + from: `"MBO Tech IT" <${process.env.SMTP_FROM}>`, + to: "jonny@mbo-tech-it.de", + replyTo: data.email, + subject: `Neuer FamilyGuard-Flyer-Download: ${data.email}`, + text: `Neuer Flyer-Download\n\nE-Mail: ${data.email}`, + html, + }, + `FamilyGuard-Flyer-Benachrichtigung ${data.email}` + ); +} + +export async function sendeFlyerLink(data: { email: string }): Promise { + const downloadLink = `${process.env.APP_URL ?? "https://mbo-tech-it.de"}/downloads/MBO_FamilyGuard_Flyer_01.pdf`; + const html = ` + + + + +
+

MBO Tech IT

+

MBO FamilyGuard

+
+
+

Ihr FamilyGuard-Flyer

+

+ Vielen Dank für Ihr Interesse an MBO FamilyGuard. Den Flyer mit allen Details + finden Sie hier zum Download: +

+ + Flyer herunterladen + +

+ Fragen? Rufen Sie uns an: +49 171 9345193 +

+
+
+

MBO Tech IT · ${process.env.APP_URL ?? "https://mbo-tech-it.de"}

+
+ +`; + + await sendWithFallback( + { + from: `"MBO Tech IT" <${process.env.SMTP_FROM}>`, + to: data.email, + subject: "Ihr MBO FamilyGuard Flyer", + text: `Hallo,\n\nvielen Dank für Ihr Interesse an MBO FamilyGuard. Den Flyer finden Sie hier:\n${downloadLink}\n\nMBO Tech IT`, + html, + }, + `FamilyGuard-Flyer-Link ${data.email}` + ); +} +``` + +- [ ] **Step 2: TypeScript-Check** + +```bash +npx tsc --noEmit +``` + +Erwartet: keine Fehler. + +- [ ] **Step 3: Commit** + +```bash +git add lib/mailer.ts +git commit -m "feat: add mailer functions for FamilyGuard flyer notification and link" +``` + +--- + +### Task 4: API-Route `POST /api/familyguard-flyer` + +**Files:** +- Create: `app/api/familyguard-flyer/route.ts` + +**Interfaces:** +- Konsumiert: `sendeFlyerBenachrichtigung`, `sendeFlyerLink` (aus Task 3), `createServiceClient` (aus `lib/supabase.ts`), Tabelle `flyer_downloads` (aus Task 2). +- Produziert: `POST /api/familyguard-flyer` — Request `{ email: string, dsgvoEinwilligung: boolean }`, Response `{ ok: true, downloadUrl: string }` oder `{ ok: false, error: string }`. Wird von Task 5 (`FlyerDownloadForm`) konsumiert. + +- [ ] **Step 1: Route-Datei erstellen** + +```ts +import { NextResponse } from "next/server"; +import { sendeFlyerBenachrichtigung, sendeFlyerLink } from "@/lib/mailer"; +import { createServiceClient } from "@/lib/supabase"; + +const FLYER_DOWNLOAD_URL = "/downloads/MBO_FamilyGuard_Flyer_01.pdf"; + +function isValidEmail(email: string): boolean { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); +} + +export async function POST(request: Request) { + let body: { email?: string; dsgvoEinwilligung?: boolean }; + try { + body = await request.json(); + } catch { + return NextResponse.json({ ok: false, error: "Ungültige Anfrage" }, { status: 400 }); + } + + const email = body.email?.trim() ?? ""; + if (!isValidEmail(email)) { + return NextResponse.json( + { ok: false, error: "Bitte geben Sie eine gültige E-Mail-Adresse ein" }, + { status: 400 } + ); + } + if (body.dsgvoEinwilligung !== true) { + return NextResponse.json( + { ok: false, error: "Bitte stimmen Sie der Datenverarbeitung zu" }, + { status: 400 } + ); + } + + try { + const db = createServiceClient(); + await db.from("flyer_downloads").insert({ email, dsgvo_einwilligung: true }); + } catch (err) { + console.error("[FamilyGuard-Flyer] Supabase insert error:", err); + } + + await sendeFlyerBenachrichtigung({ email }); + sendeFlyerLink({ email }).catch((err) => + console.error("[FamilyGuard-Flyer] Flyer-Mail an Interessenten fehlgeschlagen:", err) + ); + + return NextResponse.json({ ok: true, downloadUrl: FLYER_DOWNLOAD_URL }); +} +``` + +- [ ] **Step 2: TypeScript-Check** + +```bash +npx tsc --noEmit +``` + +Erwartet: keine Fehler. + +- [ ] **Step 3: Manueller Funktionstest gegen den Dev-Server** + +```bash +npm run dev +``` + +In einem zweiten Terminal: + +```bash +curl -s -X POST http://localhost:3000/api/familyguard-flyer \ + -H "Content-Type: application/json" \ + -d '{"email":"test@beispiel.de","dsgvoEinwilligung":true}' +``` + +Erwartet: `{"ok":true,"downloadUrl":"/downloads/MBO_FamilyGuard_Flyer_01.pdf"}` (oder ein Fehler zu SMTP/Supabase im Server-Log, falls in der lokalen Umgebung keine echten Zugangsdaten hinterlegt sind — das ist unschädlich, siehe Global Constraints). + +Zusätzlich Validierung prüfen: + +```bash +curl -s -X POST http://localhost:3000/api/familyguard-flyer \ + -H "Content-Type: application/json" \ + -d '{"email":"keine-email","dsgvoEinwilligung":true}' +``` + +Erwartet: `{"ok":false,"error":"Bitte geben Sie eine gültige E-Mail-Adresse ein"}` mit Status 400. + +- [ ] **Step 4: Commit** + +```bash +git add app/api/familyguard-flyer/route.ts +git commit -m "feat: add API route for FamilyGuard flyer email-gate download" +``` + +--- + +### Task 5: Komponente `FlyerDownloadForm` + +**Files:** +- Create: `components/FlyerDownloadForm.tsx` + +**Interfaces:** +- Konsumiert: `POST /api/familyguard-flyer` (aus Task 4). +- Produziert: `export default function FlyerDownloadForm()` (keine Props), gerendert mit `id="flyer-download"` als Root-Element. Wird von Task 6 in `app/pakete/familyguard/page.tsx` eingebunden. + +- [ ] **Step 1: Komponente erstellen** + +```tsx +"use client"; + +import { useState } from "react"; + +type Status = "idle" | "loading" | "success" | "error"; + +export default function FlyerDownloadForm() { + const [email, setEmail] = useState(""); + const [dsgvo, setDsgvo] = useState(false); + const [status, setStatus] = useState("idle"); + const [errorMsg, setErrorMsg] = useState(""); + const [downloadUrl, setDownloadUrl] = useState(""); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setStatus("loading"); + setErrorMsg(""); + + try { + const res = await fetch("/api/familyguard-flyer", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, dsgvoEinwilligung: dsgvo }), + }); + const data = await res.json(); + + if (!res.ok || !data.ok) { + setErrorMsg(data.error ?? "Unbekannter Fehler"); + setStatus("error"); + } else { + setDownloadUrl(data.downloadUrl); + setStatus("success"); + } + } catch { + setErrorMsg("Netzwerkfehler – bitte versuchen Sie es erneut."); + setStatus("error"); + } + } + + return ( +
+
+ + Flyer + +

+ FamilyGuard-Flyer herunterladen +

+

+ Alle Infos zu MBO FamilyGuard kompakt als PDF – tragen Sie Ihre E-Mail-Adresse + ein und laden Sie den Flyer direkt herunter. +

+
+ + {status === "success" ? ( +
+

+ Fertig! Der Flyer wurde Ihnen außerdem per E-Mail zugeschickt. +

+ + Flyer jetzt herunterladen + +
+ ) : ( +
+ setEmail(e.target.value)} + placeholder="max@beispiel.de" + className="w-full px-4 py-3 rounded-xl bg-slate-50 dark:bg-[#111925] border border-slate-300 dark:border-gray-700 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-600 focus:outline-none focus:border-orange-500/60 focus:ring-1 focus:ring-orange-500/20 transition-colors" + /> + + + + {status === "error" && ( +
+ {errorMsg || "Anfrage fehlgeschlagen. Bitte versuchen Sie es erneut."} +
+ )} + + +
+ )} +
+ ); +} +``` + +- [ ] **Step 2: TypeScript-Check** + +```bash +npx tsc --noEmit +``` + +Erwartet: keine Fehler. + +- [ ] **Step 3: Commit** + +```bash +git add components/FlyerDownloadForm.tsx +git commit -m "feat: add FlyerDownloadForm component" +``` + +--- + +### Task 6: `FlyerDownloadForm` in die FamilyGuard-Seite einbinden + +**Files:** +- Modify: `app/pakete/familyguard/page.tsx:1-3` (Import ergänzen), `app/pakete/familyguard/page.tsx:204-206` (Komponente einfügen) + +**Interfaces:** +- Konsumiert: `FlyerDownloadForm` (aus Task 5). + +- [ ] **Step 1: Import ergänzen** + +In `app/pakete/familyguard/page.tsx` nach der bestehenden Import-Zeile `import Logo from "@/components/Logo";` (Zeile 3) einfügen: + +```tsx +import FlyerDownloadForm from "@/components/FlyerDownloadForm"; +``` + +- [ ] **Step 2: Komponente zwischen Preis-Block und Bottom-Callout einfügen** + +Den bestehenden Codeblock + +```tsx + {/* Bottom callout */} +
+``` + +ersetzen durch: + +```tsx + + + {/* Bottom callout */} +
+``` + +- [ ] **Step 3: TypeScript-Check** + +```bash +npx tsc --noEmit +``` + +Erwartet: keine Fehler. + +- [ ] **Step 4: Manueller Test im Browser** + +```bash +npm run dev +``` + +`http://localhost:3000/pakete/familyguard` öffnen, zum Flyer-Download-Bereich scrollen, Formular ohne Checkbox absenden (Browser-Validierung muss greifen wegen `required`), dann mit gültiger E-Mail + Checkbox absenden und prüfen, dass der Download-Button erscheint und `http://localhost:3000/pakete/familyguard#flyer-download` direkt zur Sektion springt. + +- [ ] **Step 5: Commit** + +```bash +git add app/pakete/familyguard/page.tsx +git commit -m "feat: embed FlyerDownloadForm on FamilyGuard page" +``` + +--- + +### Task 7: Link zur Flyer-Sektion in der Homepage-Promo-Kachel + +**Files:** +- Modify: `components/FamilyGuardPromo.tsx:42-49` + +**Interfaces:** +- Konsumiert: Anchor `#flyer-download` auf `/pakete/familyguard` (aus Task 6). + +- [ ] **Step 1: Dritten Button ergänzen** + +Den bestehenden Block + +```tsx +
+ + FamilyGuard entdecken + + + Jetzt beraten lassen + +
+``` + +ersetzen durch: + +```tsx +
+ + FamilyGuard entdecken + + + Flyer herunterladen + + + Jetzt beraten lassen + +
+``` + +- [ ] **Step 2: TypeScript-Check** + +```bash +npx tsc --noEmit +``` + +Erwartet: keine Fehler. + +- [ ] **Step 3: Manueller Test im Browser** + +```bash +npm run dev +``` + +`http://localhost:3000/` öffnen, in der FamilyGuard-Promo-Kachel auf „Flyer herunterladen" klicken, prüfen dass die FamilyGuard-Seite direkt bei der Flyer-Download-Sektion landet. + +- [ ] **Step 4: Commit** + +```bash +git add components/FamilyGuardPromo.tsx +git commit -m "feat: link homepage FamilyGuard promo to flyer download section" +``` + +--- + +## Nach Abschluss aller Tasks + +- `npm run build` einmal komplett durchlaufen lassen, um sicherzustellen, dass Produktions-Build fehlerfrei ist. +- Jonny muss die SQL-Migration aus Task 2 in der echten Supabase-Instanz ausführen und `SMTP_*`/`APP_URL`-Env-Variablen auf dem Server prüfen, damit die Mails im Produktivbetrieb tatsächlich zugestellt werden.