diff --git a/app/api/checkout/route.ts b/app/api/checkout/route.ts
index ee61c02..ec03c62 100644
--- a/app/api/checkout/route.ts
+++ b/app/api/checkout/route.ts
@@ -11,6 +11,7 @@ import { sendOrderConfirmationEmail } from "../../lib/orderEmail";
import { normalizeVatId, isValidVatId } from "../../lib/vatId";
import { checkVatIdViaVies } from "../../lib/vies";
import { computeExemptTotals, destinationCountry, isExemptionEligibleCountry } from "../../lib/vatExemption";
+import { upsertNewsletterContact } from "../../lib/brevo";
// Plain float arithmetic on money (quantity × unitPrice summed across
// lines, a percent discount, subtracting/adding those together) drifts
@@ -376,6 +377,13 @@ export async function POST(request: Request) {
});
});
+ // Fire-and-forget, same reasoning as the confirmation email above — a
+ // failed marketing sync is not worth failing checkout over, and doesn't
+ // even need a critical alert (nothing customer-facing depends on it).
+ if (body.newsletterOptIn) {
+ upsertNewsletterContact(body.email, "checkout").catch(() => {});
+ }
+
return NextResponse.json({
ok: true,
orderNumber: order.orderNumber,
diff --git a/app/api/newsletter/subscribe/route.ts b/app/api/newsletter/subscribe/route.ts
new file mode 100644
index 0000000..3791b11
--- /dev/null
+++ b/app/api/newsletter/subscribe/route.ts
@@ -0,0 +1,29 @@
+import { NextResponse } from "next/server";
+import { upsertNewsletterContact } from "../../../lib/brevo";
+
+type SubscribeBody = {
+ email?: string;
+ consent?: boolean;
+ source?: "newsletter-page" | "newsletter-modal";
+};
+
+const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+
+export async function POST(req: Request) {
+ const body: SubscribeBody = await req.json();
+ const email = body.email?.trim() ?? "";
+
+ if (!EMAIL_PATTERN.test(email)) {
+ return NextResponse.json({ ok: false, reason: "Bitte gib eine gültige E-Mail-Adresse ein." }, { status: 400 });
+ }
+ if (!body.consent) {
+ return NextResponse.json({ ok: false, reason: "Bitte akzeptiere die Datenschutzerklärung." }, { status: 400 });
+ }
+
+ const source = body.source === "newsletter-modal" ? "newsletter-modal" : "newsletter-page";
+ const result = await upsertNewsletterContact(email, source);
+ if (!result.ok) {
+ return NextResponse.json({ ok: false, reason: "Anmeldung ist fehlgeschlagen. Bitte versuche es später erneut." }, { status: 502 });
+ }
+ return NextResponse.json({ ok: true });
+}
diff --git a/app/components/Newsletter.tsx b/app/components/Newsletter.tsx
index 670fc9e..7a22b9a 100644
--- a/app/components/Newsletter.tsx
+++ b/app/components/Newsletter.tsx
@@ -1,4 +1,6 @@
-import type { ReactNode } from "react";
+"use client";
+
+import { useState, type FormEvent, type ReactNode } from "react";
import Link from "next/link";
import Image from "next/image";
import { Reveal } from "./Reveal";
@@ -31,6 +33,34 @@ export function Newsletter({
title = <>Starte mit einer Woche voller Klarheit.>,
description = "Melde dich zum Newsletter an und erhalte die 7-Tage-Challenge, mit der du durch mehr Struktur weniger Stress spürst.",
}: NewsletterProps = {}) {
+ const [email, setEmail] = useState("");
+ const [consent, setConsent] = useState(false);
+ const [status, setStatus] = useState<"idle" | "submitting" | "success" | "error">("idle");
+ const [error, setError] = useState("");
+
+ async function handleSubmit(e: FormEvent) {
+ e.preventDefault();
+ setStatus("submitting");
+ setError("");
+ try {
+ const res = await fetch("/api/newsletter/subscribe", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ email, consent, source: "newsletter-page" }),
+ });
+ const data = await res.json();
+ if (!data.ok) {
+ setError(data.reason || "Anmeldung ist fehlgeschlagen. Bitte versuche es später erneut.");
+ setStatus("error");
+ return;
+ }
+ setStatus("success");
+ } catch {
+ setError("Anmeldung ist fehlgeschlagen. Bitte versuche es später erneut.");
+ setStatus("error");
+ }
+ }
+
return (
@@ -72,54 +102,70 @@ export function Newsletter({
{/* Right: form — takes remaining space, centered vertically from md+ */}
-
-
- {/* Input + submit button — stacked below md, side by side from md+ */}
-
-
-
-
-
- {/* Consent checkbox — required since this signup's legal
- basis is consent (email marketing), not the "Ich achte
- auf deine Daten" trust note alone. Same wording/pattern
- as NewsletterModal's checkbox. */}
-
-
- {/* Privacy note — same icon/copy/color as the other
- newsletter forms (see /challenge's EmailCapture). */}
-