Files
einfach-produktiv/app/components/Newsletter.tsx
T
Marco 3da8b75395 Fix several mobile responsive issues across Hero, Newsletter, cart, and Challenge
- Hero CTA: back to a single line below lg: (wrapping put the icon
  beside two lines and looked broken), sized down instead so the full
  phrase fits.
- Hero heading: dropped the forced <br> after "darf" at true mobile
  widths, kept it only for the sm-lg tablet range it was added for.
- Hero social proof: avatars+text now wrap and center instead of
  cramming onto one row (flex-wrap + justify-center, no fixed
  breakpoint needed).
- Newsletter card icon: stacked above the copy and centered below md:
  instead of squeezed into a row beside it.
- TrustRow: left-aligned below md: instead of centered (shop, cart, and
  every other page using this component).
- Cart line item image: full width below sm: instead of a fixed 150px
  square.
- Challenge EmailCapture (top + bottom CTA, same shared component):
  input and button stack full-width below sm: instead of squeezing
  into one row.
- Challenge steps: icon above text and centered at every breakpoint
  (previously only from lg: up), connector arrows centered to match.
2026-07-24 18:48:18 +00:00

166 lines
7.5 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 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.
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 */}
<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">
Fast geschafft! Schau kurz in dein Postfach da wartet schon eine Mail von uns.
</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>
);
}