diff --git a/src/lib/block-types.ts b/src/lib/block-types.ts index 505ea59..71b2bc0 100644 --- a/src/lib/block-types.ts +++ b/src/lib/block-types.ts @@ -428,7 +428,6 @@ export type InternalComponentId = "form-contact"; export interface InternalComponentBlockData { _type?: "internal_component"; _slug?: string; - id?: string; component?: InternalComponentId; layout?: BlockLayout; } diff --git a/src/lib/components/internal/ContactFormComponent.svelte b/src/lib/components/internal/ContactFormComponent.svelte index b2b3fac..426cd58 100644 --- a/src/lib/components/internal/ContactFormComponent.svelte +++ b/src/lib/components/internal/ContactFormComponent.svelte @@ -2,23 +2,66 @@ import Button from "$lib/ui/Button.svelte"; import TextInput from "$lib/ui/TextInput.svelte"; import Textarea from "$lib/ui/Textarea.svelte"; + import Checkbox from "$lib/ui/Checkbox.svelte"; + import { useTranslate, T } from "$lib/translations"; + import { + LIMITS, + validateContact, + type ContactErrors, + type ContactFields, + } from "./contact-form-validation"; - type State = "idle" | "pending" | "success" | "error"; + type FormState = "idle" | "pending" | "success" | "error"; + + const t = useTranslate(); let name = $state(""); let email = $state(""); + let subject = $state(""); let message = $state(""); + let consent = $state(false); + + // Honeypots — three with realistic names. Real users never see them. let honeypot = $state(""); - let pageLoadedAt = Date.now(); - let formState = $state("idle"); - let errorMessage = $state(""); + let website = $state(""); + let company = $state(""); + + const pageLoadedAt = Date.now(); + + let formState = $state("idle"); + let serverError = $state(""); + let submitAttempted = $state(false); + + const fields = $derived({ name, email, subject, message, consent }); + const errors = $derived(validateContact(fields)); + const isValid = $derived(Object.keys(errors).length === 0); + + function showError(field: keyof ContactFields): string { + if (!submitAttempted) return ""; + const e = errors[field]; + return e ? t(e.key, e.params) : ""; + } + + const messageRemaining = $derived(LIMITS.MESSAGE_MAX - message.length); + const messageHelp = $derived( + messageRemaining >= 0 + ? t(T.contact_message_remaining, { n: messageRemaining }) + : t(T.contact_message_overflow, { n: -messageRemaining }), + ); async function handleSubmit(e: SubmitEvent) { e.preventDefault(); + submitAttempted = true; if (formState === "pending") return; + if (!isValid) { + const first = Object.keys(errors)[0]; + const el = document.querySelector(`[data-field="${first}"]`); + el?.focus(); + return; + } formState = "pending"; - errorMessage = ""; + serverError = ""; try { const res = await fetch("/api/contact", { @@ -27,66 +70,141 @@ body: JSON.stringify({ name, email, + subject, message, + consent, _honeypot: honeypot, + website, + company, _client_age_ms: Date.now() - pageLoadedAt, }), }); if (res.ok) { formState = "success"; - } else { - const body = await res.json().catch(() => ({})); - errorMessage = body?.error || "Unbekannter Fehler"; - formState = "error"; + return; } + + const body = await res.json().catch(() => ({})); + if (res.status === 422) { + serverError = t(T.contact_server_error_validation); + } else if (res.status === 429) { + serverError = t(T.contact_server_error_throttle); + } else if (res.status === 403) { + serverError = t(T.contact_server_error_blocked); + } else { + serverError = body?.error || t(T.contact_server_error_generic); + } + formState = "error"; } catch { - errorMessage = "Netzwerkfehler – bitte erneut versuchen."; + serverError = t(T.contact_server_error_network); formState = "error"; } } {#if formState === "success"} -
-

Nachricht gesendet!

-

Wir melden uns so schnell wie möglich.

+
+

{t(T.contact_success_title)}

+

{t(T.contact_success_body)}

{:else} -
- -