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;
|
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 {
|
export interface WindMapBlockData {
|
||||||
_type?: "wind_map";
|
_type?: "wind_map";
|
||||||
_slug?: string;
|
_slug?: string;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
import CalendarBlock from "./blocks/CalendarBlock.svelte";
|
import CalendarBlock from "./blocks/CalendarBlock.svelte";
|
||||||
import OrganisationsBlock from "./blocks/OrganisationsBlock.svelte";
|
import OrganisationsBlock from "./blocks/OrganisationsBlock.svelte";
|
||||||
import OpnFormBlock from "./blocks/OpnFormBlock.svelte";
|
import OpnFormBlock from "./blocks/OpnFormBlock.svelte";
|
||||||
|
import InternalComponentBlock from "./blocks/InternalComponentBlock.svelte";
|
||||||
import DeadlineBannerBlock from "./blocks/DeadlineBannerBlock.svelte";
|
import DeadlineBannerBlock from "./blocks/DeadlineBannerBlock.svelte";
|
||||||
import StrommixBlock from "./blocks/StrommixBlock.svelte";
|
import StrommixBlock from "./blocks/StrommixBlock.svelte";
|
||||||
import WindkarteBlock from "./blocks/WindkarteBlock.svelte";
|
import WindkarteBlock from "./blocks/WindkarteBlock.svelte";
|
||||||
@@ -39,6 +40,7 @@
|
|||||||
CalendarBlockData,
|
CalendarBlockData,
|
||||||
OrganisationsBlockData,
|
OrganisationsBlockData,
|
||||||
OpnFormBlockData,
|
OpnFormBlockData,
|
||||||
|
InternalComponentBlockData,
|
||||||
DeadlineBannerBlockData,
|
DeadlineBannerBlockData,
|
||||||
LiveStrommixBlockData,
|
LiveStrommixBlockData,
|
||||||
WindMapBlockData,
|
WindMapBlockData,
|
||||||
@@ -86,6 +88,8 @@
|
|||||||
<OrganisationsBlock block={block as OrganisationsBlockData} />
|
<OrganisationsBlock block={block as OrganisationsBlockData} />
|
||||||
{:else if type === "opnform"}
|
{:else if type === "opnform"}
|
||||||
<OpnFormBlock block={block as OpnFormBlockData} />
|
<OpnFormBlock block={block as OpnFormBlockData} />
|
||||||
|
{:else if type === "internal_component"}
|
||||||
|
<InternalComponentBlock block={block as InternalComponentBlockData} />
|
||||||
{:else if type === "deadline_banner"}
|
{:else if type === "deadline_banner"}
|
||||||
<DeadlineBannerBlock block={block as DeadlineBannerBlockData} />
|
<DeadlineBannerBlock block={block as DeadlineBannerBlockData} />
|
||||||
{:else if type === "live_strommix"}
|
{: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