b835e8622f
NewsletterModal.tsx advertised a different offer (the 7-Tage-Challenge) than Newsletter.tsx's own description (7 Impulse) for the same signup flow. Also rewrote both descriptions to drop the vague "und gewinne Klarheit" buzzword formula and the word "Klarheit" repeated from the heading directly above it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
177 lines
8.3 KiB
TypeScript
177 lines
8.3 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 an und bekommst 7 kurze Impulse direkt ins Postfach – in ein paar Minuten gelesen, sofort im Alltag umsetzbar.",
|
||
}: NewsletterProps = {}) {
|
||
const { email, emailError, consent, handleConsentChange, status, error, successMessage, 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.
|
||
Icon+text stacked (icon on top, centered) below md: — side by
|
||
side they squeezed the text into a ~164px column on a 375px
|
||
phone (icon width + gap eating most of the card's inner
|
||
width), wrapping awkwardly. Row layout with the icon beside
|
||
the text is fine again from md+, where the fixed copy-column
|
||
width leaves real room. */}
|
||
<div className="flex flex-col items-center gap-4 text-center w-full md:flex-row md:items-start md:gap-8 md:text-left md:w-[var(--newsletter-copy-width)] md:py-4 md:shrink-0">
|
||
|
||
{/* Decorative envelope icon, tilted -4° as per design.
|
||
w-[4rem], not w-16 — this project's --spacing-16 is a
|
||
fluid token (floors to 40px below 768px, see globals.css),
|
||
so pairing w-16 with the fixed h-[3.438rem] squished the
|
||
icon to a 40:55 box on mobile instead of the SVG's native
|
||
64:55.0096 (it has preserveAspectRatio="none", so it
|
||
actually stretches to whatever box it's given — fixed
|
||
2026-07-24). */}
|
||
<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-[4rem] 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">{successMessage}</p>
|
||
) : (
|
||
<form onSubmit={handleSubmit} className="flex flex-1 flex-col gap-4 min-w-0 w-full">
|
||
|
||
{/* Input + submit button — stacked below lg: (was md:).
|
||
The card above already goes side-by-side at md: with a
|
||
fixed-width copy column (--newsletter-copy-width), which
|
||
only leaves ~200px for this form column at 768px — not
|
||
enough room for input+button side by side. Stacked
|
||
through the whole Tablet range instead, side by side
|
||
again once the form column has real room at lg:. */}
|
||
<div className="flex flex-col lg: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) => handleConsentChange(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>
|
||
);
|
||
}
|