Files
einfach-produktiv/app/components/Newsletter.tsx
T
Marco b835e8622f Align newsletter modal copy with the homepage newsletter block
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>
2026-07-29 09:58:27 +00:00

177 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}