Files
Wabenhain/src/components/newsletter-signup.tsx
T
Nick SzaunigandClaude Opus 5.5 064ca3ff6d
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 4m11s
feat(ui): Kopfzeile, Fusszeile und Details mit mehr Leben
- Kopfzeile: aktive Seite mit Honig-Unterstrich (auch im Handy-Menue),
  einlaufende Unterstriche beim Hover, beim Scrollen Glas und Schatten;
  weicht beim Runterscrollen aus und ist beim Hochscrollen sofort zurueck
  (steht still, solange Warenkorb oder Menue offen sind)
- Fusszeile mit Wabenraster und einlaufenden Link-Unterstrichen
- Mengensteller: gezeichnete Plus/Minus, taktiles Druecken, tickende Zahl
- Newsletter als eigene Flaeche mit Wabenraster; Erfolg zeichnet ein
  Haekchen in eine Wabe, Fehler werden als Alert angesagt
- 404: Wabe mit einer fehlenden Zelle als Bild fuer die fehlende Seite

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-25 08:05:01 +02:00

152 lines
6.3 KiB
TypeScript

"use client";
import { useState, type FormEvent } from "react";
import { motion } from "motion/react";
import { HoneycombPattern } from "@/components/honeycomb";
import { Container, SectionHeading, Button } from "@/components/ui";
import { ScrollReveal } from "@/components/scroll-reveal";
import { TurnstileWidget } from "@/components/turnstile-widget";
import { useI18n } from "@/i18n/i18n-provider";
type Status = "idle" | "loading" | "success" | "error";
/**
* Der Turnstile-Sitekey kommt als Prop vom Server und NICHT aus
* process.env in dieser Datei.
*
* Grund: NEXT_PUBLIC_-Variablen werden in Client Components zur BUILD-Zeit
* ins Bundle eingebacken. Der Wert wird hier aber erst zur Laufzeit gesetzt
* (k8s/01-configmap.yaml). Ein Zugriff auf process.env an dieser Stelle
* ergibt deshalb dauerhaft undefined — das Widget erschiene nie, waehrend die
* Serverseite bei gesetztem TURNSTILE_SECRET_KEY sehr wohl einen Token
* verlangt. Das Formular waere damit unabsendbar, ohne dass irgendwo ein
* Fehler auftaucht.
*
* Alle uebrigen Formulare (Login, Registrierung, Passwort-Reset, Zwei-Faktor,
* Checkout, Verfuegbarkeits-Benachrichtigung) lesen den Wert bereits im
* Server Component und reichen ihn als Prop weiter. Diese Datei folgt jetzt
* demselben Muster.
*/
export function NewsletterSignup({ turnstileSiteKey }: { turnstileSiteKey?: string }) {
const { t } = useI18n();
const [email, setEmail] = useState("");
const [status, setStatus] = useState<Status>("idle");
const [turnstileToken, setTurnstileToken] = useState("");
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setStatus("loading");
try {
const response = await fetch("/api/newsletter", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, turnstileToken }),
});
if (!response.ok) throw new Error("request failed");
setStatus("success");
setEmail("");
} catch {
setStatus("error");
}
}
return (
<section className="py-24">
<Container className="max-w-4xl">
<ScrollReveal>
{/* Eigene Flaeche mit Wabenraster: die Anmeldung ist ein Angebot,
kein Nachsatz unter der Seite. */}
<div className="relative overflow-hidden border border-honey/30 bg-honey/[0.06] px-6 py-14 text-center sm:px-14">
<HoneycombPattern className="text-honey/25" size={24} />
<div className="relative mx-auto max-w-2xl">
<SectionHeading
eyebrow={t.home.newsletter.eyebrow}
title={t.home.newsletter.title}
align="center"
className="mx-auto"
/>
<p className="mt-4 text-ink/70">{t.home.newsletter.description}</p>
{status === "success" ? (
// Bestaetigung als kleine Zeremonie: das Haekchen zeichnet sich
// in der Wabe, dann erscheint der Text.
<div className="mt-8 flex flex-col items-center" role="status">
<svg viewBox="0 0 24 26" className="h-14 w-14" fill="none" aria-hidden="true">
<motion.polygon
points="12,1 22.4,7 22.4,19 12,25 1.6,19 1.6,7"
fill="var(--color-honey)"
initial={{ scale: 0.4, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ type: "spring", stiffness: 300, damping: 16 }}
style={{ transformOrigin: "center" }}
/>
<motion.path
d="M7.5 13.2l3 3 6-6.4"
stroke="var(--color-cream)"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.4, delay: 0.25 }}
/>
</svg>
<motion.p
className="mt-4 font-serif text-2xl text-forest"
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.45 }}
>
{t.home.newsletter.successMessage}
</motion.p>
</div>
) : (
<form onSubmit={handleSubmit} className="mt-8 max-w-lg mx-auto">
{/* Eingabefeld und Knopf bilden die Zeile, das CAPTCHA steht
darunter. Frueher lagen alle drei in derselben Flex-Zeile
ohne flex-wrap: sobald das Widget tatsaechlich rendert,
belegte es rund 300 px der 512 px breiten Zeile und drueckte
das Eingabefeld auf 34 px zusammen. Solange kein Sitekey
gesetzt war, fiel das nicht auf, weil das Widget gar nicht
erst erschien. */}
<div className="flex flex-col sm:flex-row gap-3">
<input
type="email"
required
placeholder={t.home.newsletter.emailPlaceholder}
value={email}
onChange={(e) => setEmail(e.target.value)}
className="min-w-0 flex-1 border border-ink/20 bg-background px-4 py-3 text-sm focus:outline-none focus:border-forest"
/>
<Button type="submit" disabled={status === "loading"} className="flex-shrink-0 whitespace-nowrap">
{status === "loading" ? t.home.newsletter.subscribing : t.home.newsletter.subscribe}
</Button>
</div>
{turnstileSiteKey ? (
<div className="mt-3 flex justify-center">
<TurnstileWidget siteKey={turnstileSiteKey} onVerify={setTurnstileToken} />
</div>
) : null}
</form>
)}
{status === "error" ? (
<p className="mt-4 text-sm text-red-700" role="alert">
{t.home.newsletter.errorMessage}
</p>
) : null}
<p className="mt-6 text-xs text-ink/65">
{t.home.newsletter.consentBeforeLink}
<a href="/datenschutz" className="underline">
{t.home.newsletter.privacyLinkLabel}
</a>
{t.home.newsletter.consentAfterLink}
</p>
</div>
</div>
</ScrollReveal>
</Container>
</section>
);
}