MBO-Tech-IT-Webseite/docs/superpowers/plans/2026-07-21-familyguard-flye...

22 KiB
Raw Permalink Blame History

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.pdfpublic/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 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:

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

      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"

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

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

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 mit id="flyer-download" als Root-Element. Wird von Task 6 in app/pakete/familyguard/page.tsx eingebunden.

  • 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"

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

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