feat: Wabenhain Honig-Shop Anwendung
Vollstaendige Next.js/Prisma-E-Commerce-Anwendung fuer eine Imkerei: Shop, Warenkorb, Stripe-Checkout, Kundenkonto mit 2FA, Admin-Bereich (Produkte, Ernten, Chargen, Bestellungen, Gutscheine, admin-editierbare Website-Inhalte), i18n (DE/EN), Rate-Limiting/CSP/Audit-Log-Haertung, PostgreSQL- und MySQL-Unterstuetzung ueber austauschbare Prisma- Treiber-Adapter, Unit-/Komponenten-/E2E-Tests.
This commit is contained in:
1 parent
5c76fcc266
commit
c7927f2883
240 files changed
+26096
-137
No files matched your search
@@ -0,0 +1,88 @@
|
||||
"use client";
|
||||
|
||||
import { useState, type FormEvent } from "react";
|
||||
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";
|
||||
|
||||
const TURNSTILE_SITE_KEY = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY;
|
||||
|
||||
export function NewsletterSignup() {
|
||||
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-2xl text-center">
|
||||
<ScrollReveal>
|
||||
<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" ? (
|
||||
<p className="mt-8 text-forest">{t.home.newsletter.successMessage}</p>
|
||||
) : (
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="mt-8 flex flex-col sm:flex-row gap-3 max-w-lg mx-auto"
|
||||
>
|
||||
<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>
|
||||
{TURNSTILE_SITE_KEY ? (
|
||||
<div className="w-full flex justify-center">
|
||||
<TurnstileWidget siteKey={TURNSTILE_SITE_KEY} onVerify={setTurnstileToken} />
|
||||
</div>
|
||||
) : null}
|
||||
</form>
|
||||
)}
|
||||
{status === "error" ? (
|
||||
<p className="mt-4 text-sm text-red-700">{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>
|
||||
</ScrollReveal>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user