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,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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user