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:
+3
-1
@@ -8,12 +8,14 @@ const BREVO_API_URL = "https://api.brevo.com/v3/contacts";
|
||||
|
||||
export type BrevoSyncResult = { ok: true } | { ok: false; reason: string };
|
||||
|
||||
export type NewsletterOptInSource = "checkout" | "newsletter-page" | "newsletter-modal" | "newsletter-hero" | "challenge";
|
||||
|
||||
// `source` becomes a Brevo contact attribute so campaigns/segments can
|
||||
// tell a checkout opt-in apart from the standalone signup forms without
|
||||
// needing separate lists.
|
||||
export async function upsertNewsletterContact(
|
||||
email: string,
|
||||
source: "checkout" | "newsletter-page" | "newsletter-modal",
|
||||
source: NewsletterOptInSource,
|
||||
): Promise<BrevoSyncResult> {
|
||||
const apiKey = process.env.BREVO_API_KEY;
|
||||
const listId = process.env.BREVO_LIST_ID;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// Single source of truth for "is this a plausible email address" — used
|
||||
// client-side (checkout, newsletter forms) for immediate on-blur feedback
|
||||
// and server-side (newsletter subscribe route) as the same check, not a
|
||||
// second one that could drift out of sync.
|
||||
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
export function isValidEmail(value: string): boolean {
|
||||
return EMAIL_PATTERN.test(value);
|
||||
}
|
||||
|
||||
// Returns "" for valid, an error message otherwise.
|
||||
export function validateEmailFormat(value: string): string {
|
||||
if (!value.trim()) return "E-Mail-Adresse ist erforderlich.";
|
||||
return isValidEmail(value) ? "" : "Bitte eine gültige E-Mail-Adresse angeben.";
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, useState, type FormEvent } from "react";
|
||||
import { validateEmailFormat } from "./email";
|
||||
import type { NewsletterOptInSource } from "./brevo";
|
||||
|
||||
// Shared state/submit logic behind every newsletter-signup form
|
||||
// (Newsletter.tsx, NewsletterModal.tsx, WeeklyImpulsesHero.tsx's inline
|
||||
// hero form, /challenge's EmailCapture) — four places with the same
|
||||
// email+consent+submit shape but different markup/visual style, so only
|
||||
// the logic is shared here rather than a one-size-fits-all component.
|
||||
export function useNewsletterSignup(source: NewsletterOptInSource) {
|
||||
const [email, setEmail] = useState("");
|
||||
const [emailError, setEmailError] = useState("");
|
||||
const [consent, setConsent] = useState(false);
|
||||
const [status, setStatus] = useState<"idle" | "submitting" | "success" | "error">("idle");
|
||||
const [error, setError] = useState("");
|
||||
const emailRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
function handleEmailChange(value: string) {
|
||||
setEmail(value);
|
||||
if (emailError) setEmailError("");
|
||||
}
|
||||
|
||||
function handleEmailBlur(value: string) {
|
||||
setEmailError(validateEmailFormat(value));
|
||||
}
|
||||
|
||||
async function handleSubmit(e: FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
const formatError = validateEmailFormat(email);
|
||||
setEmailError(formatError);
|
||||
if (formatError) {
|
||||
emailRef.current?.focus();
|
||||
return;
|
||||
}
|
||||
setStatus("submitting");
|
||||
setError("");
|
||||
try {
|
||||
const res = await fetch("/api/newsletter/subscribe", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email, consent, source }),
|
||||
});
|
||||
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 { email, emailError, consent, setConsent, status, error, emailRef, handleEmailChange, handleEmailBlur, handleSubmit };
|
||||
}
|
||||
Reference in New Issue
Block a user