docs: add implementation plan for FamilyGuard flyer email-gate download
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LhY1QhDXGWfyhrxaJvfpNt
This commit is contained in:
parent
8ae4e341c1
commit
84599e1e08
|
|
@ -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<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:
|
||||
|
||||
```ts
|
||||
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**
|
||||
|
||||
```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<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**
|
||||
|
||||
```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 */}
|
||||
<div className="p-6 rounded-2xl border border-blue-500/20 bg-blue-500/5 text-center">
|
||||
```
|
||||
|
||||
ersetzen durch:
|
||||
|
||||
```tsx
|
||||
<FlyerDownloadForm />
|
||||
|
||||
{/* Bottom callout */}
|
||||
<div className="p-6 rounded-2xl border border-blue-500/20 bg-blue-500/5 text-center">
|
||||
```
|
||||
|
||||
- [ ] **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
|
||||
<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:
|
||||
|
||||
```tsx
|
||||
<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**
|
||||
|
||||
```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.
|
||||
Loading…
Reference in New Issue