22 KiB
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 übernpx tsc --noEmit,npm run devundcurl, wie es auch bei allen bestehenden API-Routen (z. B.app/api/contact/route.ts) gehandhabt wurde. @/*ist auf das Root-Verzeichnis aliased.JSX.Elementnicht verwenden —ReactElementausreactimportieren, 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_downloadswird nicht als Datei untermodules/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
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
npm run dev
Dann im Browser (oder mit curl in einem zweiten Terminal) prüfen:
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/downloads/MBO_FamilyGuard_Flyer_01.pdf
Erwartet: 200
- Step 3: Commit
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 demkontakt_social-Block, vor der schließenden};derTables)
Interfaces:
-
Produziert: Tabellentyp
flyer_downloadsmitRow/Insert/Update, den Task 4 (API-Route) fürdb.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:
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):
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.tsergä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:
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
npx tsc --noEmit
Erwartet: keine Fehler.
- Step 4: Commit
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, nachsendeKontaktEmail)
Interfaces:
-
Konsumiert:
sendWithFallback(bereits inlib/mailer.tsdefiniert, Zeile 40). -
Produziert:
sendeFlyerBenachrichtigung(data: { email: string }): Promise<{ sent: boolean; queued: boolean }>undsendeFlyerLink(data: { email: string }): Promise<void>, die Task 4 (API-Route) importiert. -
Step 1: Funktionen in
lib/mailer.tsergänzen
Am Ende der Datei (nach der schließenden } von sendeKontaktEmail) einfügen:
export async function sendeFlyerBenachrichtigung(
data: { email: string }
): Promise<{ sent: boolean; queued: boolean }> {
const html = `
<!DOCTYPE html>
<html lang="de">
<head><meta charset="UTF-8"></head>
<body style="font-family:system-ui,sans-serif;color:#e2e8f0;max-width:600px;margin:0 auto;padding:0">
<div style="background:#18212f;padding:20px 24px;border-bottom:2px solid #f97316">
<h1 style="color:#f97316;margin:0;font-size:20px;font-weight:700">MBO Tech IT</h1>
<p style="color:rgba(255,255,255,0.6);margin:4px 0 0;font-size:13px">FamilyGuard Flyer-Download</p>
</div>
<div style="padding:24px;background:#1e2a3b">
<h2 style="margin:0 0 16px;font-size:18px;color:#f8fafc">Neuer Flyer-Download</h2>
<p style="color:#94a3b8;margin:0 0 16px;line-height:1.6">
Ein Interessent hat den MBO FamilyGuard Flyer angefordert.
</p>
<table style="width:100%;border-collapse:collapse">
<tr><td style="padding:6px 0;color:#94a3b8;width:120px">E-Mail</td><td style="padding:6px 0"><a href="mailto:${data.email}" style="color:#60a5fa">${data.email}</a></td></tr>
</table>
</div>
<div style="padding:12px 24px;background:#111925;border-top:1px solid rgba(255,255,255,0.1)">
<p style="margin:0;font-size:11px;color:#64748b">MBO Tech IT · ${process.env.APP_URL ?? "https://mbo-tech-it.de"}</p>
</div>
</body>
</html>`;
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<void> {
const downloadLink = `${process.env.APP_URL ?? "https://mbo-tech-it.de"}/downloads/MBO_FamilyGuard_Flyer_01.pdf`;
const html = `
<!DOCTYPE html>
<html lang="de">
<head><meta charset="UTF-8"></head>
<body style="font-family:system-ui,sans-serif;color:#e2e8f0;max-width:600px;margin:0 auto;padding:0">
<div style="background:#18212f;padding:20px 24px;border-bottom:2px solid #f97316">
<h1 style="color:#f97316;margin:0;font-size:20px;font-weight:700">MBO Tech IT</h1>
<p style="color:rgba(255,255,255,0.6);margin:4px 0 0;font-size:13px">MBO FamilyGuard</p>
</div>
<div style="padding:24px;background:#1e2a3b">
<h2 style="margin:0 0 16px;font-size:18px;color:#f8fafc">Ihr FamilyGuard-Flyer</h2>
<p style="color:#94a3b8;margin:0 0 24px;line-height:1.6">
Vielen Dank für Ihr Interesse an MBO FamilyGuard. Den Flyer mit allen Details
finden Sie hier zum Download:
</p>
<a href="${downloadLink}"
style="display:inline-block;background:#f97316;color:#fff;font-weight:700;padding:12px 24px;border-radius:8px;text-decoration:none;font-size:15px">
Flyer herunterladen
</a>
<p style="color:#64748b;font-size:12px;margin:24px 0 0">
Fragen? Rufen Sie uns an: <a href="tel:+4917193451093" style="color:#f97316">+49 171 9345193</a>
</p>
</div>
<div style="padding:12px 24px;background:#111925;border-top:1px solid rgba(255,255,255,0.1)">
<p style="margin:0;font-size:11px;color:#64748b">MBO Tech IT · ${process.env.APP_URL ?? "https://mbo-tech-it.de"}</p>
</div>
</body>
</html>`;
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
npx tsc --noEmit
Erwartet: keine Fehler.
- Step 3: Commit
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(auslib/supabase.ts), Tabelleflyer_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
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
npx tsc --noEmit
Erwartet: keine Fehler.
- Step 3: Manueller Funktionstest gegen den Dev-Server
npm run dev
In einem zweiten Terminal:
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:
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
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 mitid="flyer-download"als Root-Element. Wird von Task 6 inapp/pakete/familyguard/page.tsxeingebunden. -
Step 1: Komponente erstellen
"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<Status>("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 (
<div
id="flyer-download"
className="mb-16 p-6 sm:p-8 rounded-2xl bg-white dark:bg-gray-900 border border-slate-200 dark:border-gray-800 scroll-mt-24"
>
<div className="text-center mb-6">
<span className="text-orange-400 font-mono text-xs font-bold tracking-[0.25em] uppercase">
Flyer
</span>
<h3 className="text-2xl font-black text-slate-900 dark:text-white mt-1 mb-2">
FamilyGuard-Flyer herunterladen
</h3>
<p className="text-slate-600 dark:text-slate-400 text-sm max-w-xl mx-auto">
Alle Infos zu MBO FamilyGuard kompakt als PDF – tragen Sie Ihre E-Mail-Adresse
ein und laden Sie den Flyer direkt herunter.
</p>
</div>
{status === "success" ? (
<div className="flex flex-col items-center text-center">
<p className="text-slate-700 dark:text-slate-300 text-sm mb-4">
Fertig! Der Flyer wurde Ihnen außerdem per E-Mail zugeschickt.
</p>
<a href={downloadUrl} download className="btn-primary inline-flex px-6 py-3 text-sm">
Flyer jetzt herunterladen
</a>
</div>
) : (
<form onSubmit={handleSubmit} className="max-w-md mx-auto space-y-4">
<input
type="email"
required
value={email}
onChange={(e) => 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"
/>
<label className="flex items-start gap-2 text-xs text-slate-600 dark:text-slate-400">
<input
type="checkbox"
required
checked={dsgvo}
onChange={(e) => setDsgvo(e.target.checked)}
className="mt-0.5"
/>
<span>
Ich bin damit einverstanden, dass meine E-Mail-Adresse zum Zweck des
Flyer-Versands gespeichert wird. Details in der{" "}
<a href="/datenschutz" className="text-orange-400 hover:underline">
Datenschutzerklärung
</a>
.
</span>
</label>
{status === "error" && (
<div className="flex items-start gap-3 px-4 py-3 rounded-xl bg-red-500/10 border border-red-500/20 text-red-400 text-sm">
<span>{errorMsg || "Anfrage fehlgeschlagen. Bitte versuchen Sie es erneut."}</span>
</div>
)}
<button
type="submit"
disabled={status === "loading"}
className="btn-primary w-full py-3 text-sm disabled:opacity-60 disabled:cursor-not-allowed"
>
{status === "loading" ? "Wird gesendet..." : "Flyer herunterladen"}
</button>
</form>
)}
</div>
);
}
- Step 2: TypeScript-Check
npx tsc --noEmit
Erwartet: keine Fehler.
- Step 3: Commit
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:
import FlyerDownloadForm from "@/components/FlyerDownloadForm";
- Step 2: Komponente zwischen Preis-Block und Bottom-Callout einfügen
Den bestehenden Codeblock
{/* Bottom callout */}
<div className="p-6 rounded-2xl border border-blue-500/20 bg-blue-500/5 text-center">
ersetzen durch:
<FlyerDownloadForm />
{/* Bottom callout */}
<div className="p-6 rounded-2xl border border-blue-500/20 bg-blue-500/5 text-center">
- Step 3: TypeScript-Check
npx tsc --noEmit
Erwartet: keine Fehler.
- Step 4: Manueller Test im Browser
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
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-downloadauf/pakete/familyguard(aus Task 6). -
Step 1: Dritten Button ergänzen
Den bestehenden Block
<div className="flex flex-col sm:flex-row gap-3">
<Link href="/pakete/familyguard" className="btn-primary inline-flex justify-center px-6 py-3 text-sm">
FamilyGuard entdecken
</Link>
<a href="#contact" className="btn-secondary inline-flex justify-center px-6 py-3 text-sm">
Jetzt beraten lassen
</a>
</div>
ersetzen durch:
<div className="flex flex-col sm:flex-row gap-3">
<Link href="/pakete/familyguard" className="btn-primary inline-flex justify-center px-6 py-3 text-sm">
FamilyGuard entdecken
</Link>
<Link
href="/pakete/familyguard#flyer-download"
className="btn-secondary inline-flex justify-center px-6 py-3 text-sm"
>
Flyer herunterladen
</Link>
<a href="#contact" className="btn-secondary inline-flex justify-center px-6 py-3 text-sm">
Jetzt beraten lassen
</a>
</div>
- Step 2: TypeScript-Check
npx tsc --noEmit
Erwartet: keine Fehler.
- Step 3: Manueller Test im Browser
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
git add components/FamilyGuardPromo.tsx
git commit -m "feat: link homepage FamilyGuard promo to flyer download section"
Nach Abschluss aller Tasks
npm run buildeinmal 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.