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:
2026-06-09 12:58:13 +02:00
parent ec56e907c8
commit 33fbdbc18a
2 changed files with 84 additions and 17 deletions

View File

@@ -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>

View File

@@ -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,39 +58,99 @@ 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>
<option value="Presseanfrage">Presseanfrage</option> <option value="Presseanfrage">Presseanfrage</option>
<option value="Allgemeine Anfrage">Allgemeine Anfrage</option> <option value="Allgemeine Anfrage">Allgemeine Anfrage</option>
</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>