Files
Speisekarte/components/SuggestionForm.tsx
T
florian.wilde babc55e781 Rezeptvorschläge: öffentliches Formular + Sichtung im Kantinen-Bereich
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.
2026-07-18 02:08:03 +02:00

133 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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<string | null>(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 (
<div className="flex-1 flex items-center justify-center p-4">
<div className="card w-full max-w-md">
<div className="flex justify-between items-center mb-1">
<h1 className="text-xl font-bold text-[var(--c-primary)]">
{dict.suggestions.formTitle}
</h1>
<LanguageSwitcher variant="light" />
</div>
<p className="text-sm text-gray-600 mb-4">{dict.suggestions.formHint}</p>
{success ? (
<p className="text-sm text-green-700">{dict.suggestions.success}</p>
) : (
<form onSubmit={submit} className="space-y-3">
<div>
<label className="label" htmlFor="suggestion-name">
{dict.suggestions.nameLabel}
</label>
<input
id="suggestion-name"
className="input"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
</div>
<div>
<label className="label" htmlFor="suggestion-description">
{dict.suggestions.descriptionLabel}
</label>
<textarea
id="suggestion-description"
className="input"
rows={3}
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</div>
{/* Honeypot – für Menschen unsichtbar, keine echten Nutzerdaten */}
<div className="absolute -left-[9999px]" aria-hidden="true">
<label htmlFor="website">Website</label>
<input
id="website"
name="website"
type="text"
tabIndex={-1}
autoComplete="off"
value={website}
onChange={(e) => setWebsite(e.target.value)}
/>
</div>
{error && (
<p className="text-sm text-[var(--c-del)]" role="alert">
{error}
</p>
)}
<button type="submit" className="btn btn-primary w-full" disabled={busy}>
{busy ? dict.suggestions.submitting : dict.suggestions.submit}
</button>
</form>
)}
<p className="mt-4 text-center text-sm">
<Link href="/" className="text-gray-500 hover:text-[var(--c-primary)]">
{dict.suggestions.backToMenu}
</Link>
</p>
</div>
</div>
);
}