Kontaktformular: sendet an Directus (Flow → Mail an den Verein)
- Formular POSTet an die öffentliche Directus-Collection contact_messages (Name, E-Mail, Betreff, Nachricht) + Honeypot gegen Spam + Sende-/Fehler-Status mit mailto-Fallback. Öffentliche CMS-URL hartkodiert (Build-Env wäre die interne Docker-Adresse). - Datenschutz §5 angepasst: Formular speichert jetzt in selbstgehostetem Directus und benachrichtigt den Vorstand per Mail (statt mailto). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -91,7 +91,7 @@ const settings = await getSettings();
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 class="font-bold text-[var(--color-text)] mb-3">5. Kontaktaufnahme per E-Mail</h2>
|
<h2 class="font-bold text-[var(--color-text)] mb-3">5. Kontaktaufnahme per E-Mail und Formular</h2>
|
||||||
<p>
|
<p>
|
||||||
Wenn du uns per E-Mail kontaktierst, werden deine Angaben (E-Mail-Adresse,
|
Wenn du uns per E-Mail kontaktierst, werden deine Angaben (E-Mail-Adresse,
|
||||||
ggf. Name und Nachrichteninhalt) zum Zweck der Bearbeitung deiner Anfrage
|
ggf. Name und Nachrichteninhalt) zum Zweck der Bearbeitung deiner Anfrage
|
||||||
@@ -104,8 +104,11 @@ const settings = await getSettings();
|
|||||||
Aufbewahrungspflichten entgegenstehen.
|
Aufbewahrungspflichten entgegenstehen.
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-3">
|
<p class="mt-3">
|
||||||
Das Kontaktformular auf dieser Website funktioniert als mailto-Link — es werden
|
Das Kontaktformular übermittelt deine Angaben (Name, E-Mail, Betreff, Nachricht) an
|
||||||
keine Formulardaten auf unseren Servern gespeichert oder verarbeitet.
|
unser selbst gehostetes Redaktionssystem (Directus, Server in Deutschland) und
|
||||||
|
benachrichtigt den Vorstand per E-Mail. Die Daten werden ausschließlich zur Bearbeitung
|
||||||
|
deiner Anfrage verwendet, nicht an Dritte weitergegeben und nach Erledigung gelöscht,
|
||||||
|
soweit keine gesetzlichen Aufbewahrungspflichten bestehen.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import PageHeader from "../components/PageHeader.astro";
|
|||||||
import { getSettings } from "../lib/directus";
|
import { getSettings } from "../lib/directus";
|
||||||
|
|
||||||
const settings = await getSettings();
|
const settings = await getSettings();
|
||||||
|
// Öffentliche CMS-URL für den Browser-Fetch — NICHT import.meta.env.DIRECTUS_URL,
|
||||||
|
// das ist beim Build (im Rebuild-Container) die interne Docker-Adresse.
|
||||||
|
const cmsPublicUrl = "https://cms.kitafreunde-regenbogen.de";
|
||||||
|
const contactEmail = settings?.contact_email ?? "info@kitafreunde-regenbogen.de";
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Kontakt" description="Kontakt zum Kitafreunde Regenbogen e.V. — schreib uns, werde Mitglied oder bring eine Idee ein.">
|
<Layout title="Kontakt" description="Kontakt zum Kitafreunde Regenbogen e.V. — schreib uns, werde Mitglied oder bring eine Idee ein.">
|
||||||
@@ -54,21 +58,31 @@ const settings = await getSettings();
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Formular (statisches HTML, ohne Backend) -->
|
<!-- Formular: sendet an Directus → Flow benachrichtigt den Verein per Mail -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 class="font-bold mb-4">Direkte Nachricht</h2>
|
<h2 class="font-bold mb-4">Direkte Nachricht</h2>
|
||||||
<p class="text-sm text-[var(--color-text-muted)] mb-6">
|
|
||||||
Dieses Formular öffnet deinen E-Mail-Client.
|
|
||||||
</p>
|
|
||||||
<form
|
<form
|
||||||
action={`mailto:${settings?.contact_email ?? "info@kitafreunde-regenbogen.de"}`}
|
id="contact-form"
|
||||||
method="get"
|
|
||||||
enctype="text/plain"
|
|
||||||
class="space-y-4"
|
class="space-y-4"
|
||||||
|
data-endpoint={`${cmsPublicUrl}/items/contact_messages`}
|
||||||
|
data-fallback={contactEmail}
|
||||||
>
|
>
|
||||||
|
<div class="grid sm:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="subject" class="block text-sm font-medium mb-1">Betreff</label>
|
<label for="cf-name" class="block text-sm font-medium mb-1">Name</label>
|
||||||
<select id="subject" name="subject" class="w-full border border-[var(--color-border)] rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:border-[var(--color-primary)]">
|
<input id="cf-name" name="name" type="text" required autocomplete="name"
|
||||||
|
class="w-full border border-[var(--color-border)] rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:border-[var(--color-primary)]" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="cf-email" class="block text-sm font-medium mb-1">E-Mail</label>
|
||||||
|
<input id="cf-email" name="email" type="email" required autocomplete="email"
|
||||||
|
class="w-full border border-[var(--color-border)] rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:border-[var(--color-primary)]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="cf-subject" class="block text-sm font-medium mb-1">Betreff</label>
|
||||||
|
<select id="cf-subject" name="subject"
|
||||||
|
class="w-full border border-[var(--color-border)] rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:border-[var(--color-primary)]">
|
||||||
<option value="Mitgliedschaft">Mitgliedschaft</option>
|
<option value="Mitgliedschaft">Mitgliedschaft</option>
|
||||||
<option value="Projektidee">Projektidee einbringen</option>
|
<option value="Projektidee">Projektidee einbringen</option>
|
||||||
<option value="Spende / Kooperation">Spende / Kooperation</option>
|
<option value="Spende / Kooperation">Spende / Kooperation</option>
|
||||||
@@ -77,16 +91,66 @@ const settings = await getSettings();
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="body" class="block text-sm font-medium mb-1">Nachricht</label>
|
<label for="cf-message" class="block text-sm font-medium mb-1">Nachricht</label>
|
||||||
<textarea id="body" name="body" rows="5"
|
<textarea id="cf-message" name="message" rows="5" required
|
||||||
class="w-full border border-[var(--color-border)] rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:border-[var(--color-primary)] resize-none"
|
class="w-full border border-[var(--color-border)] rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:border-[var(--color-primary)] resize-none"
|
||||||
placeholder="Deine Nachricht..."></textarea>
|
placeholder="Deine Nachricht..."></textarea>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn-primary w-full justify-center">
|
<!-- Honeypot gegen Spam: für Menschen unsichtbar, bitte leer lassen -->
|
||||||
E-Mail öffnen →
|
<div aria-hidden="true" style="position:absolute; left:-9999px; width:1px; height:1px; overflow:hidden;">
|
||||||
|
<label>Website<input type="text" name="website" tabindex="-1" autocomplete="off" /></label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" id="cf-submit" class="btn-primary w-full justify-center">
|
||||||
|
<span aria-hidden="true">✉</span> Nachricht senden
|
||||||
</button>
|
</button>
|
||||||
|
<p id="cf-status" class="text-sm hidden" role="status" aria-live="polite"></p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const form = document.getElementById("contact-form");
|
||||||
|
if (form) {
|
||||||
|
const statusEl = document.getElementById("cf-status");
|
||||||
|
const btn = document.getElementById("cf-submit");
|
||||||
|
const setStatus = (msg, color) => {
|
||||||
|
statusEl.innerHTML = msg;
|
||||||
|
statusEl.style.color = color;
|
||||||
|
statusEl.classList.remove("hidden");
|
||||||
|
};
|
||||||
|
form.addEventListener("submit", async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const data = new FormData(form);
|
||||||
|
if (data.get("website")) return; // Honeypot: stiller Bot-Abbruch
|
||||||
|
const endpoint = form.dataset.endpoint;
|
||||||
|
const fallback = form.dataset.fallback;
|
||||||
|
const payload = {
|
||||||
|
name: (data.get("name") || "").toString().trim(),
|
||||||
|
email: (data.get("email") || "").toString().trim(),
|
||||||
|
subject: (data.get("subject") || "").toString(),
|
||||||
|
message: (data.get("message") || "").toString().trim(),
|
||||||
|
};
|
||||||
|
btn.disabled = true;
|
||||||
|
setStatus("Senden…", "var(--color-text-muted)");
|
||||||
|
try {
|
||||||
|
const res = await fetch(endpoint, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||||
|
form.reset();
|
||||||
|
setStatus("Danke! Deine Nachricht ist angekommen — wir melden uns bald.", "var(--color-rb-green)");
|
||||||
|
} catch (err) {
|
||||||
|
setStatus(
|
||||||
|
`Senden hat gerade nicht geklappt. Schreib uns gern direkt: <a class="underline font-medium" href="mailto:${fallback}">${fallback}</a>`,
|
||||||
|
"var(--color-rb-red)",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
Reference in New Issue
Block a user