c852752006
- Newsletter.tsx: max-w-[1280px] mx-auto, matching /lebensuhr's own newsletter-style section and Footer.tsx's existing 1280px convention — previously unbounded and stretched edge-to-edge on wide viewports. - Product pages: noShippingCost now shows "Keine Versandkosten" instead of just omitting "zzgl. Versand" silently. - /versand's exemption note no longer implies only digital products can be exempt — noShippingCost is a general per-product flag. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
188 lines
9.1 KiB
TypeScript
188 lines
9.1 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 = "Kleine Impulse – große Wirkung",
|
||
description = "Melde dich an und bekomme meine Sonntags-Impulse ab jetzt jede Woche direkt in dein Postfach: kurze Gedanken, praktische Impulse und kleine Anstöße für mehr Klarheit im Alltag.",
|
||
}: 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">
|
||
|
||
{/* max-w-[1280px] mx-auto — the consistent default width for this
|
||
card, matching /lebensuhr's own newsletter-style "Bottom CTA"
|
||
section (same 1280px cap Footer.tsx already uses) and every
|
||
other narrow-viewport-friendly section on the site. Previously
|
||
unbounded here, so on wide desktop viewports this card stretched
|
||
edge-to-edge while /lebensuhr's version stayed centered and
|
||
noticeably narrower — reported 2026-07-29. */}
|
||
{/* Rounded card: cream bg, stacks below sm (moved down from the old md:) */}
|
||
<Reveal className="bg-bg-muted flex flex-col sm:flex-row gap-8 sm:gap-12 items-center px-8 py-8 sm:py-0 rounded-md w-full max-w-[1280px] mx-auto">
|
||
|
||
{/* Left: copy — fixed width from sm+ so the form always gets the remaining space.
|
||
Icon+text stacked (icon on top, centered) below sm: — side by
|
||
side they squeezed the text into a narrow column on a 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 sm+, where the fixed copy-column width
|
||
leaves real room. */}
|
||
<div className="flex flex-col items-center gap-4 text-center w-full sm:flex-row sm:items-start sm:gap-8 sm:text-left sm:w-[var(--newsletter-copy-width)] sm:py-4 sm: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 sm+ */}
|
||
<div className="flex w-full sm:flex-1 items-center sm: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:, a deliberate
|
||
exception to the site's sm: (640px) structural
|
||
consolidation, not a leftover of it. The card above
|
||
already goes side-by-side at sm: with a fixed-width
|
||
copy column (--newsletter-copy-width), which only
|
||
leaves ~128px for this form column at 640px and
|
||
~197px at 768px — not enough room for input+button
|
||
side by side even at the new, lower floor. Stacked
|
||
through the whole 640-1023px range instead, side by
|
||
side again once the form column has real room (≈333px)
|
||
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>
|
||
);
|
||
}
|