789a818c6b
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>
160 lines
7.0 KiB
TypeScript
160 lines
7.0 KiB
TypeScript
"use client";
|
|
|
|
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
|
|
// own wording/color for this trust note.
|
|
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>
|
|
);
|
|
}
|
|
|
|
type NewsletterProps = {
|
|
title?: ReactNode;
|
|
description?: string;
|
|
};
|
|
|
|
/**
|
|
* Reused as-is (same bordered/muted panel + icon-decoration + form
|
|
* pattern) on both Home and /newsletter (the "Impulse & Tipps" page) —
|
|
* their Figma frames use the exact same newsletter-inner component
|
|
* instance, just with different copy, so title/description are props
|
|
* instead of a second near-duplicate component.
|
|
*/
|
|
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, emailError, consent, setConsent, status, error, emailRef, handleEmailChange, handleEmailBlur, handleSubmit } =
|
|
useNewsletterSignup("newsletter-page");
|
|
|
|
return (
|
|
<section className="py-16 w-full">
|
|
|
|
{/* Outer section padding — same fluid horizontal padding as all other sections */}
|
|
<div className="px-[var(--layout-padding-x)] w-full">
|
|
|
|
{/* Rounded card: cream bg, stacks below md */}
|
|
<Reveal className="bg-bg-muted flex flex-col md:flex-row gap-8 md:gap-12 items-center px-8 py-8 md:py-0 rounded-md w-full">
|
|
|
|
{/* Left: copy — fixed width from md+ so the form always gets the remaining space */}
|
|
<div className="flex gap-8 items-start w-full md:w-[var(--newsletter-copy-width)] md:py-4 md:shrink-0">
|
|
|
|
{/* Decorative envelope icon, tilted -4° as per design */}
|
|
<div className="flex items-center justify-center shrink-0 w-[4.23rem] h-[3.71rem]">
|
|
<div className="-rotate-4 -scale-y-100">
|
|
<Image
|
|
alt=""
|
|
src="/newsletter-icon.svg"
|
|
width={64}
|
|
height={55}
|
|
className="w-16 h-[3.438rem] block"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Heading + body */}
|
|
<div className="flex flex-col gap-4 flex-1 min-w-0 text-text-primary">
|
|
<p
|
|
className="font-semibold text-h-section leading-normal"
|
|
style={{ fontFamily: "var(--font-lora)" }}
|
|
>
|
|
{title}
|
|
</p>
|
|
<p className="font-normal text-body leading-6">
|
|
{description}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right: form — takes remaining space, centered vertically from md+ */}
|
|
<div className="flex w-full md:flex-1 items-center md:self-stretch min-w-0">
|
|
{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-1 flex-col gap-4 min-w-0 w-full">
|
|
|
|
{/* 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) => 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-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"
|
|
disabled={status === "submitting"}
|
|
className="shrink-0 bg-brand rounded-sm px-5 py-3 font-bold text-h4 text-text-primary tracking-[0.18px] whitespace-nowrap hover:brightness-95 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-muted 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 -mt-2">{emailError}</p>
|
|
)}
|
|
|
|
{/* 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. */}
|
|
<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>
|
|
|
|
{status === "error" && (
|
|
<p className="text-label text-red-600 font-normal">{error}</p>
|
|
)}
|
|
|
|
{/* Privacy note — same icon/copy/color as the other
|
|
newsletter forms (see /challenge's EmailCapture). */}
|
|
<p className="flex items-center gap-1.5 text-label text-[#888] font-normal leading-normal">
|
|
<LockIcon />
|
|
Keine Werbung. Jederzeit abbestellbar.
|
|
</p>
|
|
</form>
|
|
)}
|
|
</div>
|
|
|
|
</Reveal>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|