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:
@@ -422,6 +422,17 @@ export type CalendarBlockData = Omit<
|
||||
layout?: BlockLayout;
|
||||
};
|
||||
|
||||
/** Internal component block (_type: "internal_component"). */
|
||||
export type InternalComponentId = "form-contact";
|
||||
|
||||
export interface InternalComponentBlockData {
|
||||
_type?: "internal_component";
|
||||
_slug?: string;
|
||||
id?: string;
|
||||
component?: InternalComponentId;
|
||||
layout?: BlockLayout;
|
||||
}
|
||||
|
||||
export interface WindMapBlockData {
|
||||
_type?: "wind_map";
|
||||
_slug?: string;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import CalendarBlock from "./blocks/CalendarBlock.svelte";
|
||||
import OrganisationsBlock from "./blocks/OrganisationsBlock.svelte";
|
||||
import OpnFormBlock from "./blocks/OpnFormBlock.svelte";
|
||||
import InternalComponentBlock from "./blocks/InternalComponentBlock.svelte";
|
||||
import DeadlineBannerBlock from "./blocks/DeadlineBannerBlock.svelte";
|
||||
import StrommixBlock from "./blocks/StrommixBlock.svelte";
|
||||
import WindkarteBlock from "./blocks/WindkarteBlock.svelte";
|
||||
@@ -39,6 +40,7 @@
|
||||
CalendarBlockData,
|
||||
OrganisationsBlockData,
|
||||
OpnFormBlockData,
|
||||
InternalComponentBlockData,
|
||||
DeadlineBannerBlockData,
|
||||
LiveStrommixBlockData,
|
||||
WindMapBlockData,
|
||||
@@ -86,6 +88,8 @@
|
||||
<OrganisationsBlock block={block as OrganisationsBlockData} />
|
||||
{:else if type === "opnform"}
|
||||
<OpnFormBlock block={block as OpnFormBlockData} />
|
||||
{:else if type === "internal_component"}
|
||||
<InternalComponentBlock block={block as InternalComponentBlockData} />
|
||||
{:else if type === "deadline_banner"}
|
||||
<DeadlineBannerBlock block={block as DeadlineBannerBlockData} />
|
||||
{:else if type === "live_strommix"}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { getBlockLayoutClasses } from "$lib/block-layout";
|
||||
import type { InternalComponentBlockData } from "$lib/block-types";
|
||||
import ContactFormComponent from "$lib/components/internal/ContactFormComponent.svelte";
|
||||
|
||||
let { block }: { block: InternalComponentBlockData } = $props();
|
||||
|
||||
const layoutClasses = $derived(getBlockLayoutClasses(block.layout));
|
||||
const component = $derived(block.component ?? "");
|
||||
</script>
|
||||
|
||||
<div class={layoutClasses} data-block="InternalComponent" data-block-type="internal_component" data-block-slug={block._slug}>
|
||||
{#if component === "form-contact"}
|
||||
<ContactFormComponent />
|
||||
{:else}
|
||||
<p class="text-sm text-stein-500">Unbekannte interne Komponente: <code>{component}</code></p>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -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}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { json } from "@sveltejs/kit";
|
||||
import { env } from "$env/dynamic/private";
|
||||
import type { RequestHandler } from "./$types";
|
||||
|
||||
export const POST: RequestHandler = async ({ request, getClientAddress }) => {
|
||||
const body = await request.json().catch(() => null);
|
||||
if (!body || typeof body !== "object") {
|
||||
return json({ error: "Ungültige Anfrage" }, { status: 400 });
|
||||
}
|
||||
|
||||
const cmsBase = (env.PUBLIC_CMS_URL || "http://localhost:3000").replace(/\/$/, "");
|
||||
const cmsEnv = env.PUBLIC_CMS_ENV || "";
|
||||
const url = cmsEnv
|
||||
? `${cmsBase}/api/forms/kontakt/submit?_environment=${encodeURIComponent(cmsEnv)}`
|
||||
: `${cmsBase}/api/forms/kontakt/submit`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Forwarded-For": getClientAddress(),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
return json({ ok: true }, { status: 201 });
|
||||
}
|
||||
|
||||
const text = await res.text().catch(() => "");
|
||||
return json({ error: text || "Fehler beim Senden" }, { status: res.status });
|
||||
};
|
||||
Reference in New Issue
Block a user