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]>
159 lines
6.3 KiB
TypeScript
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>
|
|
);
|
|
}
|