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:
@@ -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 });
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useNewsletterSignup } from "../../lib/useNewsletterSignup";
|
||||
|
||||
function LockIcon() {
|
||||
return (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" className="shrink-0">
|
||||
<rect x="2" y="6" width="10" height="7" rx="1.5" stroke="#888" strokeWidth="1.3" />
|
||||
<path d="M4.5 6V4.5a2.5 2.5 0 0 1 5 0V6" stroke="#888" strokeWidth="1.3" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function EmailCapture({ buttonLabel = "Challenge starten" }: { buttonLabel?: string }) {
|
||||
const { email, emailError, consent, setConsent, status, error, emailRef, handleEmailChange, handleEmailBlur, handleSubmit } =
|
||||
useNewsletterSignup("challenge");
|
||||
|
||||
if (status === "success") {
|
||||
return <p className="text-[1rem] text-[#222221] font-medium">Danke! Du bist jetzt für den Newsletter angemeldet.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-2 w-full">
|
||||
<div className="flex gap-3 w-full">
|
||||
<input
|
||||
ref={emailRef}
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => handleEmailChange(e.target.value)}
|
||||
onBlur={(e) => handleEmailBlur(e.target.value)}
|
||||
placeholder="Deine E-Mail-Adresse"
|
||||
aria-invalid={Boolean(emailError)}
|
||||
className={`flex-1 min-w-0 bg-white border rounded-lg px-4 py-3 text-[1rem] text-[#868686] outline-none transition-colors ${
|
||||
emailError ? "border-red-600 focus:border-red-600" : "border-[#d9d9d9] focus:border-[#f6a701]"
|
||||
}`}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={status === "submitting"}
|
||||
className="shrink-0 bg-[#f6a701] rounded-lg px-5 py-3 font-bold text-[1rem] text-[#222221] whitespace-nowrap hover:brightness-95 active:scale-[0.97] transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#f6a701] focus-visible:ring-offset-2 disabled:opacity-60 disabled:pointer-events-none"
|
||||
>
|
||||
{status === "submitting" ? "Wird gesendet…" : buttonLabel}
|
||||
</button>
|
||||
</div>
|
||||
{emailError && <p className="text-[0.8rem] text-red-600">{emailError}</p>}
|
||||
{/* Consent checkbox — this signup's legal basis is consent (email
|
||||
marketing), same wording as the other newsletter forms; colors
|
||||
match this page's own hardcoded palette instead of the shared
|
||||
design tokens, consistent with the rest of the page. */}
|
||||
<label className="flex gap-2 items-start cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
required
|
||||
checked={consent}
|
||||
onChange={(e) => setConsent(e.target.checked)}
|
||||
className="size-4 shrink-0 mt-0.5 rounded-xs border border-[#d9d9d9] accent-[#f6a701]"
|
||||
/>
|
||||
<span className="text-[0.8rem] text-[#444] leading-normal">
|
||||
Ich akzeptiere die{" "}
|
||||
<Link
|
||||
href="/datenschutz"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-[#f6a701]"
|
||||
>
|
||||
Datenschutzerklärung
|
||||
</Link>
|
||||
.
|
||||
</span>
|
||||
</label>
|
||||
{status === "error" && <p className="text-[0.8rem] text-red-600">{error}</p>}
|
||||
<p className="flex items-center gap-1.5 text-[0.8rem] text-[#888]">
|
||||
<LockIcon />
|
||||
Keine Werbung. Jederzeit abbestellbar.
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
+1
-55
@@ -7,6 +7,7 @@ import { Reveal, RevealGroup, RevealItem } from "../components/Reveal";
|
||||
import { TestimonialsGrid } from "../components/TestimonialsGrid";
|
||||
import { LiveTestimonialsGrid } from "../components/LiveTestimonialsGrid";
|
||||
import { getTestimonials } from "../lib/payload";
|
||||
import { EmailCapture } from "./components/EmailCapture";
|
||||
|
||||
const title = "7-Tage-Challenge – Mehr Klarheit in 7 Tagen";
|
||||
const description =
|
||||
@@ -82,15 +83,6 @@ function Check() {
|
||||
);
|
||||
}
|
||||
|
||||
function LockIcon() {
|
||||
return (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" className="shrink-0">
|
||||
<rect x="2" y="6" width="10" height="7" rx="1.5" stroke="#888" strokeWidth="1.3" />
|
||||
<path d="M4.5 6V4.5a2.5 2.5 0 0 1 5 0V6" stroke="#888" strokeWidth="1.3" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const steps = [
|
||||
{
|
||||
icon: <IconEnvelope />,
|
||||
@@ -122,52 +114,6 @@ const benefits = [
|
||||
{ title: "Gelassener leben", desc: "Weniger Stress, mehr Zeit für die Dinge, die dir wichtig sind." },
|
||||
];
|
||||
|
||||
function EmailCapture({ buttonLabel = "Challenge starten" }: { buttonLabel?: string }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<div className="flex gap-3 w-full">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Deine E-Mail-Adresse"
|
||||
className="flex-1 min-w-0 bg-white border border-[#d9d9d9] rounded-lg px-4 py-3 text-[1rem] text-[#868686] outline-none focus:border-[#f6a701] transition-colors"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="shrink-0 bg-[#f6a701] rounded-lg px-5 py-3 font-bold text-[1rem] text-[#222221] whitespace-nowrap hover:brightness-95 active:scale-[0.97] transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#f6a701] focus-visible:ring-offset-2"
|
||||
>
|
||||
{buttonLabel}
|
||||
</button>
|
||||
</div>
|
||||
{/* Consent checkbox — this signup's legal basis is consent (email
|
||||
marketing), same wording as the other newsletter forms; colors
|
||||
match this page's own hardcoded palette instead of the shared
|
||||
design tokens, consistent with the rest of the page. */}
|
||||
<label className="flex gap-2 items-start cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="size-4 shrink-0 mt-0.5 rounded-xs border border-[#d9d9d9] accent-[#f6a701]"
|
||||
/>
|
||||
<span className="text-[0.8rem] text-[#444] leading-normal">
|
||||
Ich akzeptiere die{" "}
|
||||
<Link
|
||||
href="/datenschutz"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-[#f6a701]"
|
||||
>
|
||||
Datenschutzerklärung
|
||||
</Link>
|
||||
.
|
||||
</span>
|
||||
</label>
|
||||
<p className="flex items-center gap-1.5 text-[0.8rem] text-[#888]">
|
||||
<LockIcon />
|
||||
Keine Werbung. Jederzeit abbestellbar.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function ChallengePage() {
|
||||
const { isEnabled: isPreview } = await draftMode();
|
||||
const testimonials = await getTestimonials("challenge", { draft: isPreview });
|
||||
|
||||
@@ -19,6 +19,7 @@ import { dispatchAuthChanged } from "../../lib/auth";
|
||||
import { readCheckoutDraft, writeCheckoutDraft, clearCheckoutDraft } from "../../lib/checkoutDraft";
|
||||
import { normalizeVatId, isValidVatId } from "../../lib/vatId";
|
||||
import { computeExemptTotals, destinationCountry, isExemptionEligibleCountry } from "../../lib/vatExemption";
|
||||
import { validateEmailFormat } from "../../lib/email";
|
||||
import type { ShippingMethod, ShippingCountry, PaymentMethod, TrustBadge, ShippingSettings } from "../../lib/payload";
|
||||
import type { CustomerProfile } from "../../lib/customerAuth";
|
||||
|
||||
@@ -44,11 +45,6 @@ function validateRequired(label: string, value: string): string {
|
||||
return value.trim() ? "" : `${label} ist erforderlich.`;
|
||||
}
|
||||
|
||||
function validateEmailFormat(value: string): string {
|
||||
if (!value.trim()) return "E-Mail-Adresse ist erforderlich.";
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? "" : "Bitte eine gültige E-Mail-Adresse angeben.";
|
||||
}
|
||||
|
||||
function validateZip(value: string, country: string, plzDigitsMap: Record<string, number>): string {
|
||||
if (!value.trim()) return "PLZ ist erforderlich.";
|
||||
const digits = plzDigitsMap[country] ?? 4;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useState, type FormEvent, type ReactNode } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { Reveal } from "./Reveal";
|
||||
import { useNewsletterSignup } from "../lib/useNewsletterSignup";
|
||||
|
||||
// Same lock icon + copy as /challenge's and /newsletter's EmailCapture —
|
||||
// unified across all newsletter-signup forms instead of each having its
|
||||
@@ -33,33 +34,8 @@ export function Newsletter({
|
||||
title = <>Starte mit einer Woche voller Klarheit<span className="text-brand">.</span></>,
|
||||
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<HTMLFormElement>) {
|
||||
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");
|
||||
}
|
||||
}
|
||||
const { email, emailError, consent, setConsent, status, error, emailRef, handleEmailChange, handleEmailBlur, handleSubmit } =
|
||||
useNewsletterSignup("newsletter-page");
|
||||
|
||||
return (
|
||||
<section className="py-16 w-full">
|
||||
@@ -112,12 +88,17 @@ export function Newsletter({
|
||||
{/* Input + submit button — stacked below md, side by side from md+ */}
|
||||
<div className="flex flex-col md:flex-row gap-4 items-stretch w-full">
|
||||
<input
|
||||
ref={emailRef}
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
onChange={(e) => handleEmailChange(e.target.value)}
|
||||
onBlur={(e) => handleEmailBlur(e.target.value)}
|
||||
placeholder="Deine E-Mail-Adresse"
|
||||
className="flex-1 min-w-0 bg-bg-white border border-border rounded-sm px-6 py-3 text-body text-text-muted font-normal outline-none focus:border-brand transition-colors"
|
||||
aria-invalid={Boolean(emailError)}
|
||||
className={`flex-1 min-w-0 bg-bg-white border rounded-sm px-6 py-3 text-body text-text-muted font-normal outline-none transition-colors ${
|
||||
emailError ? "border-red-600 focus:border-red-600" : "border-border focus:border-brand"
|
||||
}`}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
@@ -127,6 +108,9 @@ export function Newsletter({
|
||||
{status === "submitting" ? "Wird gesendet…" : "Jetzt anmelden"}
|
||||
</button>
|
||||
</div>
|
||||
{emailError && (
|
||||
<p className="text-label text-red-600 font-normal -mt-2">{emailError}</p>
|
||||
)}
|
||||
|
||||
{/* Consent checkbox — required since this signup's legal
|
||||
basis is consent (email marketing), not the "Ich achte
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState, type FormEvent } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useNewsletterSignup } from "../lib/useNewsletterSignup";
|
||||
|
||||
const features = [
|
||||
{
|
||||
@@ -34,33 +35,8 @@ const features = [
|
||||
export function NewsletterModal({ open, onClose }: { open: boolean; onClose: () => void }) {
|
||||
const dialogRef = useRef<HTMLDivElement>(null);
|
||||
const closeButtonRef = useRef<HTMLButtonElement>(null);
|
||||
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<HTMLFormElement>) {
|
||||
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-modal" }),
|
||||
});
|
||||
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");
|
||||
}
|
||||
}
|
||||
const { email, emailError, consent, setConsent, status, error, emailRef, handleEmailChange, handleEmailBlur, handleSubmit } =
|
||||
useNewsletterSignup("newsletter-modal");
|
||||
|
||||
// Background scroll lock while open — intercepts and cancels the wheel/
|
||||
// touch input that would cause scrolling, instead of toggling
|
||||
@@ -221,13 +197,21 @@ export function NewsletterModal({ open, onClose }: { open: boolean; onClose: ()
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-5 items-start w-full">
|
||||
<div className="flex flex-col gap-4 items-start w-full">
|
||||
<input
|
||||
ref={emailRef}
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
onChange={(e) => handleEmailChange(e.target.value)}
|
||||
onBlur={(e) => handleEmailBlur(e.target.value)}
|
||||
placeholder="Deine E-Mail-Adresse"
|
||||
className="w-full bg-bg-white border border-border rounded-sm px-6 py-3 text-body text-text-muted font-normal outline-none focus:border-brand transition-colors"
|
||||
aria-invalid={Boolean(emailError)}
|
||||
className={`w-full bg-bg-white border rounded-sm px-6 py-3 text-body text-text-muted font-normal outline-none transition-colors ${
|
||||
emailError ? "border-red-600 focus:border-red-600" : "border-border focus:border-brand"
|
||||
}`}
|
||||
/>
|
||||
{emailError && (
|
||||
<p className="text-label text-red-600 font-normal -mt-2">{emailError}</p>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={status === "submitting"}
|
||||
|
||||
+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 };
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { Reveal } from "../../components/Reveal";
|
||||
import { useNewsletterSignup } from "../../lib/useNewsletterSignup";
|
||||
|
||||
// Same lock icon + copy as /challenge's and the shared Newsletter
|
||||
// component's trust note — unified across all newsletter-signup forms.
|
||||
@@ -21,6 +24,9 @@ const checklist = [
|
||||
];
|
||||
|
||||
export function WeeklyImpulsesHero() {
|
||||
const { email, emailError, consent, setConsent, status, error, emailRef, handleEmailChange, handleEmailBlur, handleSubmit } =
|
||||
useNewsletterSignup("newsletter-hero");
|
||||
|
||||
return (
|
||||
<section className="bg-bg-base w-full overflow-hidden">
|
||||
{/* Same lg:-only structural exception as Home/todo-cards Hero (see
|
||||
@@ -93,46 +99,73 @@ export function WeeklyImpulsesHero() {
|
||||
{/* Inline email capture — page-specific, simpler than the shared
|
||||
Newsletter component's panel form (no button-adjacent styling
|
||||
needed here, just input + submit inline). */}
|
||||
<div className="flex gap-3 items-start w-full sm:w-auto">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Deine E-Mail-Adresse"
|
||||
className="w-full sm:w-[17.5rem] bg-bg-base border border-border rounded-sm px-4 py-[0.8125rem] text-body-sm text-text-muted font-normal outline-none focus:border-brand transition-colors"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="shrink-0 bg-brand rounded-sm px-6 py-[0.8125rem] font-bold text-body text-text-primary whitespace-nowrap hover:bg-brand-hover active:scale-[0.97] transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2 focus-visible:ring-offset-bg-base"
|
||||
>
|
||||
Jetzt anmelden
|
||||
</button>
|
||||
</div>
|
||||
{status === "success" ? (
|
||||
<p className="text-body text-text-primary font-medium">
|
||||
Danke! Du bist jetzt für den Newsletter angemeldet.
|
||||
</p>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-3 items-start w-full">
|
||||
<div className="flex gap-3 items-start w-full sm:w-auto">
|
||||
<input
|
||||
ref={emailRef}
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => handleEmailChange(e.target.value)}
|
||||
onBlur={(e) => handleEmailBlur(e.target.value)}
|
||||
placeholder="Deine E-Mail-Adresse"
|
||||
aria-invalid={Boolean(emailError)}
|
||||
className={`w-full sm:w-[17.5rem] bg-bg-base border rounded-sm px-4 py-[0.8125rem] text-body-sm text-text-muted font-normal outline-none transition-colors ${
|
||||
emailError ? "border-red-600 focus:border-red-600" : "border-border focus:border-brand"
|
||||
}`}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={status === "submitting"}
|
||||
className="shrink-0 bg-brand rounded-sm px-6 py-[0.8125rem] font-bold text-body text-text-primary whitespace-nowrap hover:bg-brand-hover active:scale-[0.97] transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2 focus-visible:ring-offset-bg-base disabled:opacity-60 disabled:pointer-events-none"
|
||||
>
|
||||
{status === "submitting" ? "Wird gesendet…" : "Jetzt anmelden"}
|
||||
</button>
|
||||
</div>
|
||||
{emailError && (
|
||||
<p className="text-label text-red-600 font-normal">{emailError}</p>
|
||||
)}
|
||||
|
||||
{/* Consent checkbox — this signup's legal basis is consent
|
||||
(email marketing), same wording/pattern as the shared
|
||||
Newsletter component's and NewsletterModal's checkbox. */}
|
||||
<label className="flex gap-2 items-start cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="size-4 shrink-0 mt-0.5 rounded-xs border border-border accent-brand"
|
||||
/>
|
||||
<span className="text-label text-text-primary font-normal leading-normal">
|
||||
Ich akzeptiere die{" "}
|
||||
<Link
|
||||
href="/datenschutz"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-brand"
|
||||
>
|
||||
Datenschutzerklärung
|
||||
</Link>
|
||||
.
|
||||
</span>
|
||||
</label>
|
||||
{/* Consent checkbox — this signup's legal basis is consent
|
||||
(email marketing), same wording/pattern as the shared
|
||||
Newsletter component's and NewsletterModal's checkbox. */}
|
||||
<label className="flex gap-2 items-start cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
required
|
||||
checked={consent}
|
||||
onChange={(e) => setConsent(e.target.checked)}
|
||||
className="size-4 shrink-0 mt-0.5 rounded-xs border border-border accent-brand"
|
||||
/>
|
||||
<span className="text-label text-text-primary font-normal leading-normal">
|
||||
Ich akzeptiere die{" "}
|
||||
<Link
|
||||
href="/datenschutz"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-brand"
|
||||
>
|
||||
Datenschutzerklärung
|
||||
</Link>
|
||||
.
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div className="flex gap-[0.375rem] items-center">
|
||||
<LockIcon />
|
||||
<span className="text-label text-[#888]">Keine Werbung. Jederzeit abbestellbar.</span>
|
||||
</div>
|
||||
{status === "error" && (
|
||||
<p className="text-label text-red-600 font-normal">{error}</p>
|
||||
)}
|
||||
|
||||
<div className="flex gap-[0.375rem] items-center">
|
||||
<LockIcon />
|
||||
<span className="text-label text-[#888]">Keine Werbung. Jederzeit abbestellbar.</span>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user