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:
@@ -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}
|
||||
Reference in New Issue
Block a user