feat: add internal_component block type with contact form

InternalComponentBlock dispatches on `component` field value.
ContactFormComponent submits via /api/contact (SvelteKit proxy →
CMS /api/forms/kontakt/submit) with honeypot + client_age_ms.
BlockRenderer and block-types.ts updated accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Peter Meier
2026-05-21 11:01:47 +02:00
parent 50d68a17ca
commit 9e05fbb2ba
5 changed files with 157 additions and 0 deletions
@@ -0,0 +1,92 @@
<script lang="ts">
import Button from "$lib/ui/Button.svelte";
import TextInput from "$lib/ui/TextInput.svelte";
import Textarea from "$lib/ui/Textarea.svelte";
type State = "idle" | "pending" | "success" | "error";
let name = $state("");
let email = $state("");
let message = $state("");
let honeypot = $state("");
let pageLoadedAt = Date.now();
let formState = $state<State>("idle");
let errorMessage = $state("");
async function handleSubmit(e: SubmitEvent) {
e.preventDefault();
if (formState === "pending") return;
formState = "pending";
errorMessage = "";
try {
const res = await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name,
email,
message,
_honeypot: honeypot,
_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";
}
} catch {
errorMessage = "Netzwerkfehler bitte erneut versuchen.";
formState = "error";
}
}
</script>
{#if formState === "success"}
<div class="rounded-lg border border-wald-300 bg-wald-50 px-6 py-8 text-center">
<p class="text-lg font-semibold text-wald-700">Nachricht gesendet!</p>
<p class="mt-1 text-sm text-stein-600">Wir melden uns so schnell wie möglich.</p>
</div>
{:else}
<form onsubmit={handleSubmit} novalidate class="space-y-4">
<!-- Honeypot per CSS versteckt, von Bots ausgefüllt -->
<div aria-hidden="true" style="position:absolute;left:-9999px;opacity:0;pointer-events:none;tab-index:-1;">
<label for="contact-hp">Website</label>
<input id="contact-hp" name="_honeypot" tabindex="-1" autocomplete="off" bind:value={honeypot} />
</div>
<TextInput
name="name"
label="Name"
placeholder="Dein Name"
required
bind:value={name}
/>
<TextInput
name="email"
label="E-Mail"
placeholder="deine@email.de"
required
bind:value={email}
/>
<Textarea
name="message"
label="Nachricht"
placeholder="Deine Nachricht …"
required
rows={5}
bind:value={message}
/>
{#if formState === "error"}
<p class="text-sm text-error" role="alert">{errorMessage}</p>
{/if}
<Button type="submit" variant="primary" loading={formState === "pending"} label="Senden" />
</form>
{/if}