Add on-blur email validation to every newsletter signup form

Extends the checkout pattern (inline red error text, refocus on
submit if invalid) to all four newsletter-signup entry points. Two of
them (WeeklyImpulsesHero's inline hero form on /newsletter, and
/challenge's EmailCapture) turned out to be completely non-functional
before this too — same static-markup-with-no-onSubmit issue as
Newsletter.tsx/NewsletterModal.tsx had, just missed in the previous
pass since they're separate components sharing only the visual
pattern, not the code.

Consolidated the shared email+consent+submit state (previously
duplicated per-component) into useNewsletterSignup.ts, and pulled the
plain email-format regex (previously duplicated in CheckoutContent.tsx
and the subscribe route) into lib/email.ts as a single source of
truth. /challenge's EmailCapture is now its own client component
(app/challenge/components/EmailCapture.tsx) since its parent page is
an async Server Component and can't hold form state itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-07-23 20:48:13 +00:00
parent 6a4539bf9b
commit 789a818c6b
10 changed files with 264 additions and 164 deletions
+6 -5
View File
@@ -1,26 +1,27 @@
import { NextResponse } from "next/server";
import { upsertNewsletterContact } from "../../../lib/brevo";
import { upsertNewsletterContact, type NewsletterOptInSource } from "../../../lib/brevo";
import { isValidEmail } from "../../../lib/email";
type SubscribeBody = {
email?: string;
consent?: boolean;
source?: "newsletter-page" | "newsletter-modal";
source?: NewsletterOptInSource;
};
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const VALID_SOURCES: NewsletterOptInSource[] = ["newsletter-page", "newsletter-modal", "newsletter-hero", "challenge"];
export async function POST(req: Request) {
const body: SubscribeBody = await req.json();
const email = body.email?.trim() ?? "";
if (!EMAIL_PATTERN.test(email)) {
if (!isValidEmail(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 source = body.source && VALID_SOURCES.includes(body.source) ? body.source : "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 });