Files
Wabenhain/src/components/business-inquiry-form.tsx
T
Nick SzaunigandClaude Opus 5.5 774bc9cf00 feat(ui): Pakete fuer Firmenkunden als Auswahl-Karten im Formular
Die drei Pakete standen doppelt: als Deko-Kacheln (mit grossem
Waben-Wasserzeichen) ueber dem Formular und darin noch einmal als
Auswahlliste. Jetzt waehlt man sie direkt im Formular als Karten mit
sichtbarem Auswahl- und Fokuszustand; die Kacheln entfallen. Feldname
und Werte (package = KLASSIK/GESCHENK/INDIVIDUELL) sind unveraendert.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 02:11:29 +02:00

159 lines
6.3 KiB
TypeScript

"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";
/**
* 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 BusinessInquiryForm({ turnstileSiteKey }: { turnstileSiteKey?: string }) {
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>
{/* Die Pakete als waehlbare Karten direkt im Formular — frueher standen
sie als Deko-Kacheln ueber dem Formular und darin noch einmal als
Auswahlliste. Gleicher Feldname und dieselben Werte wie zuvor. */}
<fieldset className="text-sm">
<legend className="text-ink/70">{t.businessInquiryForm.packageLabel}</legend>
<div className="mt-2 grid gap-3 sm:grid-cols-3">
{t.unternehmenPage.packages.map((p, i) => (
<label
key={p.key}
className="group relative flex cursor-pointer flex-col border border-ink/20 bg-background p-4 transition-colors duration-300 hover:border-ink/40 has-[:checked]:border-forest has-[:checked]:bg-forest/[0.04] has-[:focus-visible]:outline has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-honey-dark"
>
<input type="radio" name="package" value={p.key} defaultChecked={i === 0} className="sr-only" />
<span className="flex items-center justify-between gap-3">
<span className="font-serif text-xl text-forest">{p.title}</span>
<span
aria-hidden="true"
className="h-3.5 w-3.5 flex-shrink-0 rounded-full border border-ink/30 transition-[border-width,border-color] duration-200 group-has-[:checked]:border-[4.5px] group-has-[:checked]:border-forest"
/>
</span>
<span className="mt-2 text-ink/70 leading-relaxed">{p.text}</span>
</label>
))}
</div>
</fieldset>
<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}
{turnstileSiteKey ? (
<TurnstileWidget siteKey={turnstileSiteKey} 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>
);
}