# 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.