From babc55e7814d30a2ba0ec2eebf9900f7bc0fd80c Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Sat, 18 Jul 2026 02:08:03 +0200 Subject: [PATCH] =?UTF-8?q?Rezeptvorschl=C3=A4ge:=20=C3=B6ffentliches=20Fo?= =?UTF-8?q?rmular=20+=20Sichtung=20im=20Kantinen-Bereich?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nicht angemeldete Besucher können unter /vorschlag ein fehlendes Gericht vorschlagen (Name + optionale Beschreibung); Dublettenprüfung gegen bestehende Gerichte, einfacher Honeypot gegen Bot-Spam. Staff/ Admin sichten eingereichte Vorschläge unter /intern/vorschlaege und übernehmen sie als Gericht oder lehnen sie ab. --- app/intern/vorschlaege/page.tsx | 5 ++ app/vorschlag/page.tsx | 5 ++ components/SuggestionForm.tsx | 132 ++++++++++++++++++++++++++++++ components/SuggestionReview.tsx | 137 ++++++++++++++++++++++++++++++++ 4 files changed, 279 insertions(+) create mode 100644 app/intern/vorschlaege/page.tsx create mode 100644 app/vorschlag/page.tsx create mode 100644 components/SuggestionForm.tsx create mode 100644 components/SuggestionReview.tsx diff --git a/app/intern/vorschlaege/page.tsx b/app/intern/vorschlaege/page.tsx new file mode 100644 index 0000000..d07475e --- /dev/null +++ b/app/intern/vorschlaege/page.tsx @@ -0,0 +1,5 @@ +import SuggestionReview from "@/components/SuggestionReview"; + +export default function VorschlaegePage() { + return ; +} diff --git a/app/vorschlag/page.tsx b/app/vorschlag/page.tsx new file mode 100644 index 0000000..91c4d6e --- /dev/null +++ b/app/vorschlag/page.tsx @@ -0,0 +1,5 @@ +import SuggestionForm from "@/components/SuggestionForm"; + +export default function VorschlagPage() { + return ; +} diff --git a/components/SuggestionForm.tsx b/components/SuggestionForm.tsx new file mode 100644 index 0000000..390f0b3 --- /dev/null +++ b/components/SuggestionForm.tsx @@ -0,0 +1,132 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { createClient } from "@/lib/supabase/client"; +import { formatMessage } from "@/lib/i18n/shared"; +import { useDict } from "@/components/i18n/LocaleProvider"; +import LanguageSwitcher from "@/components/i18n/LanguageSwitcher"; + +export default function SuggestionForm() { + const supabase = createClient(); + const dict = useDict(); + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + // Honeypot: für echte Nutzer unsichtbares Feld. Füllt ein Bot es aus, + // tun wir so, als sei alles gut gegangen, ohne wirklich zu speichern. + const [website, setWebsite] = useState(""); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + const [busy, setBusy] = useState(false); + + async function submit(e: React.FormEvent) { + e.preventDefault(); + setError(null); + const trimmedName = name.trim(); + if (!trimmedName) { + setError(dict.suggestions.nameRequired); + return; + } + if (website.trim()) { + // Bot-Falle ausgelöst: keine echte Anfrage, aber Erfolg vortäuschen. + setSuccess(true); + return; + } + setBusy(true); + + const { data: existing } = await supabase + .from("dishes") + .select("id") + .ilike("name", trimmedName) + .limit(1); + if (existing && existing.length > 0) { + setError(dict.suggestions.duplicate); + setBusy(false); + return; + } + + const { error: insertError } = await supabase.from("dish_suggestions").insert({ + name: trimmedName, + description: description.trim() || null, + }); + if (insertError) { + setError(formatMessage(dict.suggestions.genericError, { message: insertError.message })); + setBusy(false); + return; + } + setSuccess(true); + setBusy(false); + } + + return ( +
+
+
+

+ {dict.suggestions.formTitle} +

+ +
+

{dict.suggestions.formHint}

+ + {success ? ( +

{dict.suggestions.success}

+ ) : ( +
+
+ + setName(e.target.value)} + required + /> +
+
+ +