Files
florian.wilde a9a9cf7ba4 v1.5: Alle Manager-Komponenten auf eigene API-Routen umgestellt
Neue interne Route Handler unter app/api/ (dishes inkl. Bulk-Delete,
sections, allergens, additives, menu-entries, dish-suggestions,
settings, admin/branding) ersetzen den direkten Browser→Postgrest-
Zugriff der bisherigen Supabase-Anbindung. Jede Route prüft Autorisierung
selbst (lib/auth/session.ts requireRole) statt sich auf Postgres-RLS zu
verlassen – ersetzt 1:1 die bisherigen Policies (öffentlich lesbar /
staff+admin schreiben / nur admin für Einstellungen & Benutzer).

Rezeptvorschlag-Dublettencheck (SuggestionForm) läuft jetzt zwingend
serverseitig im POST-Handler, da es keinen Browser→Postgres-Pfad mehr
gibt. "Passkey annehmen" (SuggestionReview) läuft transaktional
(Gericht anlegen + Vorschlag löschen in einem Schritt).

Logo/Favicon: neuer lokaler Dateispeicher (lib/branding.ts, Docker-
Volume) statt Supabase-Storage-Bucket, ausgeliefert über
app/branding/[filename]. logoUrl()/faviconUrl() liefern jetzt einfache
relative Pfade statt einer fest verdrahteten Supabase-Storage-URL.

DeepL-Route (api/translate) und Benutzerverwaltung (api/admin/users)
laufen jetzt gegen die users-Tabelle statt Supabase Auth.

Alle "use client"-Manager-Komponenten (DishManager, WeekEditor,
SectionManager, CodeListManager, SuggestionForm/Review, UserManager,
DesignEditor) rufen ihre jeweiligen /api/-Routen per fetch() statt den
Supabase-Browser-Client zu nutzen.
2026-07-18 11:01:52 +02:00

128 lines
4.2 KiB
TypeScript
Raw Permalink 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 { formatMessage } from "@/lib/i18n/shared";
import { useDict } from "@/components/i18n/LocaleProvider";
import LanguageSwitcher from "@/components/i18n/LanguageSwitcher";
export default function SuggestionForm() {
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 res = await fetch("/api/dish-suggestions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: trimmedName, description: description.trim() || null }),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
setError(
data.error === "duplicate"
? dict.suggestions.duplicate
: formatMessage(dict.suggestions.genericError, {
message: data.error ?? String(res.status),
})
);
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>
);
}