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:
Nick Szaunig committed 2026-08-17 14:30:28 +02:00
1 parent 5c76fcc266
commit c7927f2883
240 files changed
+26096 -137

No files matched your search

+132
View File
@@ -0,0 +1,132 @@
"use client";
import { useState, type FormEvent } from "react";
import { Button } from "@/components/ui";
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 BusinessInquiryForm() {
const { t } = useI18n();
const [status, setStatus] = useState<Status>("idle");
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [turnstileToken, setTurnstileToken] = useState("");
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setStatus("loading");
setErrorMessage(null);
const formData = new FormData(event.currentTarget);
const payload = {
companyName: String(formData.get("companyName") ?? ""),
contactPerson: String(formData.get("contactPerson") ?? ""),
email: String(formData.get("email") ?? ""),
phone: String(formData.get("phone") ?? "") || undefined,
desiredQuantity: String(formData.get("desiredQuantity") ?? ""),
desiredDeliveryDate: String(formData.get("desiredDeliveryDate") ?? "") || undefined,
package: String(formData.get("package") ?? "KLASSIK"),
message: String(formData.get("message") ?? "") || undefined,
turnstileToken,
};
try {
const response = await fetch("/api/business-inquiry", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) throw new Error("request failed");
setStatus("success");
event.currentTarget.reset();
} catch {
setStatus("error");
setErrorMessage(t.businessInquiryForm.errorMessage);
}
}
if (status === "success") {
return (
<p className="text-forest">
{t.businessInquiryForm.successMessage}
</p>
);
}
return (
<form onSubmit={handleSubmit} className="space-y-5">
<div className="grid sm:grid-cols-2 gap-4">
<TextField name="companyName" label={t.businessInquiryForm.companyLabel} required />
<TextField name="contactPerson" label={t.businessInquiryForm.contactPersonLabel} required />
</div>
<div className="grid sm:grid-cols-2 gap-4">
<TextField name="email" label={t.businessInquiryForm.emailLabel} type="email" required />
<TextField name="phone" label={t.businessInquiryForm.phoneOptionalLabel} />
</div>
<div className="grid sm:grid-cols-2 gap-4">
<TextField name="desiredQuantity" label={t.businessInquiryForm.quantityLabel} type="number" required />
<TextField name="desiredDeliveryDate" label={t.businessInquiryForm.deliveryDateLabel} type="date" />
</div>
<label className="block text-sm">
<span className="text-ink/70">{t.businessInquiryForm.packageLabel}</span>
<select
name="package"
className="mt-1 w-full border border-ink/20 bg-background px-4 py-3"
>
{t.unternehmenPage.packages.map((p) => (
<option key={p.key} value={p.key}>
{p.title} — {p.text}
</option>
))}
</select>
</label>
<label className="block text-sm">
<span className="text-ink/70">{t.businessInquiryForm.messageLabel}</span>
<textarea
name="message"
rows={4}
className="mt-1 w-full border border-ink/20 bg-background px-4 py-3"
/>
</label>
{errorMessage ? <p className="text-sm text-red-700">{errorMessage}</p> : null}
{TURNSTILE_SITE_KEY ? (
<TurnstileWidget siteKey={TURNSTILE_SITE_KEY} onVerify={setTurnstileToken} />
) : null}
<Button type="submit" disabled={status === "loading"}>
{status === "loading" ? t.businessInquiryForm.submitting : t.businessInquiryForm.submit}
</Button>
</form>
);
}
function TextField({
name,
label,
type = "text",
required = false,
}: {
name: string;
label: string;
type?: string;
required?: boolean;
}) {
return (
<label className="block text-sm">
<span className="text-ink/70">{label}</span>
<input
name={name}
type={type}
required={required}
className="mt-1 w-full border border-ink/20 bg-background px-4 py-3 focus:outline-none focus:border-forest"
/>
</label>
);
}