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} + ) : ( + + + + {dict.suggestions.nameLabel} + + setName(e.target.value)} + required + /> + + + + {dict.suggestions.descriptionLabel} + + setDescription(e.target.value)} + /> + + {/* Honeypot – für Menschen unsichtbar, keine echten Nutzerdaten */} + + Website + setWebsite(e.target.value)} + /> + + {error && ( + + {error} + + )} + + {busy ? dict.suggestions.submitting : dict.suggestions.submit} + + + )} + + + + {dict.suggestions.backToMenu} + + + + + ); +} diff --git a/components/SuggestionReview.tsx b/components/SuggestionReview.tsx new file mode 100644 index 0000000..17fa909 --- /dev/null +++ b/components/SuggestionReview.tsx @@ -0,0 +1,137 @@ +"use client"; + +import { useCallback, useEffect, useMemo, useState } from "react"; +import { createClient } from "@/lib/supabase/client"; +import { formatMessage } from "@/lib/i18n/shared"; +import { useDict } from "@/components/i18n/LocaleProvider"; +import type { DishSuggestion } from "@/lib/types"; + +export default function SuggestionReview() { + const supabase = useMemo(() => createClient(), []); + const dict = useDict(); + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [info, setInfo] = useState(null); + const [busyId, setBusyId] = useState(null); + + const load = useCallback(async () => { + const { data, error } = await supabase + .from("dish_suggestions") + .select("*") + .order("created_at", { ascending: false }); + if (error) + setError(formatMessage(dict.suggestions.loadError, { message: error.message })); + else setItems(data as DishSuggestion[]); + setLoading(false); + }, [supabase, dict]); + + useEffect(() => { + load(); + }, [load]); + + async function accept(item: DishSuggestion) { + setBusyId(item.id); + setError(null); + setInfo(null); + const { error: insertError } = await supabase.from("dishes").insert({ + name: item.name, + price: 0, + }); + if (insertError) { + setError( + formatMessage(dict.suggestions.actionError, { message: insertError.message }) + ); + setBusyId(null); + return; + } + const { error: deleteError } = await supabase + .from("dish_suggestions") + .delete() + .eq("id", item.id); + if (deleteError) { + setError( + formatMessage(dict.suggestions.actionError, { message: deleteError.message }) + ); + setBusyId(null); + return; + } + setInfo(dict.suggestions.accepted); + setBusyId(null); + await load(); + } + + async function reject(item: DishSuggestion) { + if (!window.confirm(formatMessage(dict.suggestions.confirmReject, { name: item.name }))) + return; + setBusyId(item.id); + setError(null); + setInfo(null); + const { error } = await supabase.from("dish_suggestions").delete().eq("id", item.id); + if (error) { + setError(formatMessage(dict.suggestions.actionError, { message: error.message })); + } else { + setInfo(dict.suggestions.rejected); + } + setBusyId(null); + await load(); + } + + return ( + + + {dict.suggestions.reviewTitle} + + + {error && ( + + {error} + + )} + {info && {info}} + + {loading ? ( + {dict.common.loading} + ) : ( + + {items.map((item) => ( + + + {item.name} + {item.description && ( + + {item.description} + + )} + + {formatMessage(dict.suggestions.submittedOn, { + date: new Date(item.created_at).toLocaleDateString(dict.dateLocale), + })} + + + accept(item)} + disabled={busyId === item.id} + > + {dict.suggestions.accept} + + reject(item)} + disabled={busyId === item.id} + > + {dict.suggestions.reject} + + + ))} + {items.length === 0 && ( + {dict.suggestions.reviewEmpty} + )} + + )} + + ); +}
{dict.suggestions.formHint}
{dict.suggestions.success}
+ {error} +
+ + {dict.suggestions.backToMenu} + +
{info}
{dict.common.loading}