diff --git a/components/CodeListManager.tsx b/components/CodeListManager.tsx
index 1a677dd..0529ad4 100644
--- a/components/CodeListManager.tsx
+++ b/components/CodeListManager.tsx
@@ -3,8 +3,7 @@
// Pflege der globalen Allergen-/Zusatzstofflisten, die unterhalb
// der öffentlichen Speisekarte angezeigt werden.
-import { useCallback, useEffect, useMemo, useState } from "react";
-import { createClient } from "@/lib/supabase/client";
+import { useCallback, useEffect, useState } from "react";
import { formatMessage } from "@/lib/i18n/shared";
import { localized } from "@/lib/types";
import type { CodeItem } from "@/lib/types";
@@ -17,7 +16,6 @@ export default function CodeListManager({
table: "allergens" | "additives";
translateEnabled: boolean;
}) {
- const supabase = useMemo(() => createClient(), []);
const dict = useDict();
const locale = useLocale();
const title =
@@ -41,10 +39,10 @@ export default function CodeListManager({
);
const load = useCallback(async () => {
- const { data, error } = await supabase.from(table).select("*").order("sort");
- if (error) setError(formatMessage(dict.codeList.loadError, { message: error.message }));
- else setItems(data as CodeItem[]);
- }, [supabase, table, dict]);
+ const res = await fetch(`/api/${table}`);
+ if (!res.ok) setError(formatMessage(dict.codeList.loadError, { message: String(res.status) }));
+ else setItems(await res.json());
+ }, [table, dict]);
useEffect(() => {
load();
@@ -64,23 +62,23 @@ export default function CodeListManager({
name_pl: editing.namePl.trim() || null,
};
- let dbError;
- if (editing.original) {
- ({ error: dbError } = await supabase
- .from(table)
- .update(payload)
- .eq("code", editing.original));
- } else {
- const maxSort = items.reduce((m, i) => Math.max(m, i.sort), 0);
- ({ error: dbError } = await supabase
- .from(table)
- .insert({ ...payload, sort: maxSort + 1 }));
- }
- if (dbError) {
+ const res = editing.original
+ ? await fetch(`/api/${table}/${editing.original}`, {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload),
+ })
+ : await fetch(`/api/${table}`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload),
+ });
+ if (!res.ok) {
+ const data = await res.json().catch(() => ({}));
setError(
- /duplicate key/i.test(dbError.message)
+ data.error === "duplicate_code"
? formatMessage(dict.codeList.duplicateCode, { code })
- : formatMessage(dict.codeList.saveError, { message: dbError.message })
+ : formatMessage(dict.codeList.saveError, { message: data.error ?? String(res.status) })
);
return;
}
@@ -95,9 +93,9 @@ export default function CodeListManager({
)
)
return;
- const { error } = await supabase.from(table).delete().eq("code", item.code);
- if (error)
- setError(formatMessage(dict.codeList.deleteError, { message: error.message }));
+ const res = await fetch(`/api/${table}/${item.code}`, { method: "DELETE" });
+ if (!res.ok)
+ setError(formatMessage(dict.codeList.deleteError, { message: String(res.status) }));
await load();
}
diff --git a/components/DesignEditor.tsx b/components/DesignEditor.tsx
index 11b83c5..8c71063 100644
--- a/components/DesignEditor.tsx
+++ b/components/DesignEditor.tsx
@@ -1,21 +1,14 @@
"use client";
-import { useCallback, useEffect, useMemo, useState } from "react";
+import { useCallback, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
-import { createClient } from "@/lib/supabase/client";
import { formatMessage } from "@/lib/i18n/shared";
import { DEFAULT_COLORS, faviconUrl, logoUrl } from "@/lib/types";
import { useDict } from "@/components/i18n/LocaleProvider";
type AssetKind = "logo" | "favicon";
-const ASSET_COLUMN: Record
= {
- logo: "logo_path",
- favicon: "favicon_path",
-};
-
export default function DesignEditor() {
- const supabase = useMemo(() => createClient(), []);
const router = useRouter();
const dict = useDict();
const colorLabels: Record = {
@@ -36,19 +29,16 @@ export default function DesignEditor() {
const [busy, setBusy] = useState(false);
const load = useCallback(async () => {
- const { data, error } = await supabase
- .from("settings")
- .select("colors, logo_path, favicon_path")
- .eq("id", 1)
- .maybeSingle();
- if (error) {
- setError(formatMessage(dict.designEditor.loadError, { message: error.message }));
+ const res = await fetch("/api/settings");
+ if (!res.ok) {
+ setError(formatMessage(dict.designEditor.loadError, { message: String(res.status) }));
return;
}
+ const data = await res.json();
setColors({ ...DEFAULT_COLORS, ...(data?.colors ?? {}) });
setLogoPath(data?.logo_path ?? null);
setFaviconPath(data?.favicon_path ?? null);
- }, [supabase, dict]);
+ }, [dict]);
useEffect(() => {
load();
@@ -58,12 +48,13 @@ export default function DesignEditor() {
setBusy(true);
setError(null);
setInfo(null);
- const { error } = await supabase
- .from("settings")
- .update({ colors })
- .eq("id", 1);
- if (error)
- setError(formatMessage(dict.designEditor.saveError, { message: error.message }));
+ const res = await fetch("/api/settings", {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ colors }),
+ });
+ if (!res.ok)
+ setError(formatMessage(dict.designEditor.saveError, { message: String(res.status) }));
else {
setInfo(dict.designEditor.colorsSaved);
router.refresh();
@@ -87,28 +78,21 @@ export default function DesignEditor() {
setError(null);
setInfo(null);
- const oldPath = kind === "logo" ? logoPath : faviconPath;
- const ext = file.name.split(".").pop()?.toLowerCase() ?? "png";
- const path = `${kind}-${Date.now()}.${ext}`;
- const { error: uploadError } = await supabase.storage
- .from("branding")
- .upload(path, file, { contentType: file.type });
- if (uploadError) {
- setError(formatMessage(dict.designEditor.uploadError, { message: uploadError.message }));
+ const formData = new FormData();
+ formData.append("kind", kind);
+ formData.append("file", file);
+ const res = await fetch("/api/admin/branding", { method: "POST", body: formData });
+ if (!res.ok) {
+ const data = await res.json().catch(() => ({}));
+ setError(
+ formatMessage(dict.designEditor.uploadError, {
+ message: data.error ?? String(res.status),
+ })
+ );
setBusy(false);
return;
}
-
- const { error: settingsError } = await supabase
- .from("settings")
- .update({ [ASSET_COLUMN[kind]]: path })
- .eq("id", 1);
- if (settingsError) {
- setError(formatMessage(dict.designEditor.saveError, { message: settingsError.message }));
- setBusy(false);
- return;
- }
- if (oldPath) await supabase.storage.from("branding").remove([oldPath]);
+ const { path } = await res.json();
if (kind === "logo") setLogoPath(path);
else setFaviconPath(path);
@@ -130,14 +114,14 @@ export default function DesignEditor() {
return;
setBusy(true);
setError(null);
- const { error } = await supabase
- .from("settings")
- .update({ [ASSET_COLUMN[kind]]: null })
- .eq("id", 1);
- if (error) {
- setError(formatMessage(dict.designEditor.saveError, { message: error.message }));
+ const res = await fetch("/api/admin/branding", {
+ method: "DELETE",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ kind }),
+ });
+ if (!res.ok) {
+ setError(formatMessage(dict.designEditor.saveError, { message: String(res.status) }));
} else {
- await supabase.storage.from("branding").remove([path]);
if (kind === "logo") setLogoPath(null);
else setFaviconPath(null);
setInfo(kind === "logo" ? dict.designEditor.logoRemoved : dict.designEditor.faviconRemoved);
diff --git a/components/DishManager.tsx b/components/DishManager.tsx
index cd69b7f..820297a 100644
--- a/components/DishManager.tsx
+++ b/components/DishManager.tsx
@@ -1,7 +1,6 @@
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
-import { createClient } from "@/lib/supabase/client";
import { dishCodes, formatPrice } from "@/lib/menu";
import { formatMessage } from "@/lib/i18n/shared";
import { localized } from "@/lib/types";
@@ -34,7 +33,6 @@ export default function DishManager({
}: {
translateEnabled: boolean;
}) {
- const supabase = useMemo(() => createClient(), []);
const dict = useDict();
const locale = useLocale();
const [dishes, setDishes] = useState([]);
@@ -58,20 +56,19 @@ export default function DishManager({
const load = useCallback(async () => {
const [dishesRes, allergensRes, additivesRes] = await Promise.all([
- supabase.from("dishes").select("*").order("name"),
- supabase.from("allergens").select("*").order("sort"),
- supabase.from("additives").select("*").order("sort"),
+ fetch("/api/dishes"),
+ fetch("/api/allergens"),
+ fetch("/api/additives"),
]);
- const problem = dishesRes.error ?? allergensRes.error ?? additivesRes.error;
- if (problem)
- setError(formatMessage(dict.dishManager.loadError, { message: problem.message }));
- else {
- setDishes(dishesRes.data as Dish[]);
- setAllergens(allergensRes.data as CodeItem[]);
- setAdditives(additivesRes.data as CodeItem[]);
+ if (!dishesRes.ok || !allergensRes.ok || !additivesRes.ok) {
+ setError(formatMessage(dict.dishManager.loadError, { message: "load failed" }));
+ } else {
+ setDishes(await dishesRes.json());
+ setAllergens(await allergensRes.json());
+ setAdditives(await additivesRes.json());
}
setLoading(false);
- }, [supabase, dict]);
+ }, [dict]);
useEffect(() => {
load();
@@ -94,11 +91,19 @@ export default function DishManager({
allergens: form.allergens,
additives: form.additives,
};
- const { error } = form.id
- ? await supabase.from("dishes").update(payload).eq("id", form.id)
- : await supabase.from("dishes").insert(payload);
- if (error) {
- setError(formatMessage(dict.dishManager.saveError, { message: error.message }));
+ const res = form.id
+ ? await fetch(`/api/dishes/${form.id}`, {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload),
+ })
+ : await fetch("/api/dishes", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload),
+ });
+ if (!res.ok) {
+ setError(formatMessage(dict.dishManager.saveError, { message: String(res.status) }));
return;
}
setForm(null);
@@ -112,9 +117,9 @@ export default function DishManager({
)
)
return;
- const { error } = await supabase.from("dishes").delete().eq("id", dish.id);
- if (error)
- setError(formatMessage(dict.dishManager.deleteError, { message: error.message }));
+ const res = await fetch(`/api/dishes/${dish.id}`, { method: "DELETE" });
+ if (!res.ok)
+ setError(formatMessage(dict.dishManager.deleteError, { message: String(res.status) }));
setSelected((prev) => {
const next = new Set(prev);
next.delete(dish.id);
@@ -156,12 +161,13 @@ export default function DishManager({
return;
setBulkBusy(true);
setError(null);
- const { error } = await supabase
- .from("dishes")
- .delete()
- .in("id", Array.from(selected));
- if (error)
- setError(formatMessage(dict.dishManager.deleteError, { message: error.message }));
+ const res = await fetch("/api/dishes/bulk-delete", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ ids: Array.from(selected) }),
+ });
+ if (!res.ok)
+ setError(formatMessage(dict.dishManager.deleteError, { message: String(res.status) }));
setSelected(new Set());
setBulkBusy(false);
await load();
diff --git a/components/SectionManager.tsx b/components/SectionManager.tsx
index 2c714e5..6ae91ac 100644
--- a/components/SectionManager.tsx
+++ b/components/SectionManager.tsx
@@ -3,8 +3,7 @@
// Verwaltung der Essenszeiten (Brotzeit, Mittagessen, …):
// anlegen, umbenennen, Uhrzeit ändern, löschen, Reihenfolge anpassen.
-import { useCallback, useEffect, useMemo, useState } from "react";
-import { createClient } from "@/lib/supabase/client";
+import { useCallback, useEffect, useState } from "react";
import { formatMessage } from "@/lib/i18n/shared";
import { localized } from "@/lib/types";
import type { Section } from "@/lib/types";
@@ -34,7 +33,6 @@ function splitTimeRange(range: string): { from: string; to: string } {
}
export default function SectionManager() {
- const supabase = useMemo(() => createClient(), []);
const dict = useDict();
const locale = useLocale();
const [sections, setSections] = useState([]);
@@ -44,12 +42,12 @@ export default function SectionManager() {
const [busy, setBusy] = useState(false);
const load = useCallback(async () => {
- const { data, error } = await supabase.from("sections").select("*").order("sort");
- if (error)
- setError(formatMessage(dict.sectionManager.loadError, { message: error.message }));
- else setSections(data as Section[]);
+ const res = await fetch("/api/sections");
+ if (!res.ok)
+ setError(formatMessage(dict.sectionManager.loadError, { message: String(res.status) }));
+ else setSections(await res.json());
setLoading(false);
- }, [supabase, dict]);
+ }, [dict]);
useEffect(() => {
load();
@@ -68,27 +66,19 @@ export default function SectionManager() {
time_range: `${form.timeFrom} - ${form.timeTo}`,
};
- let dbError;
- if (form.key) {
- ({ error: dbError } = await supabase
- .from("sections")
- .update(payload)
- .eq("key", form.key));
- } else {
- // Frisch aus der DB lesen statt den lokalen State zu nutzen, damit
- // ein veralteter Snapshot nicht zu doppelten sort-Werten führt.
- const { data: maxRow } = await supabase
- .from("sections")
- .select("sort")
- .order("sort", { ascending: false })
- .limit(1)
- .maybeSingle();
- ({ error: dbError } = await supabase
- .from("sections")
- .insert({ ...payload, sort: (maxRow?.sort ?? 0) + 1 }));
- }
- if (dbError) {
- setError(formatMessage(dict.sectionManager.saveError, { message: dbError.message }));
+ const res = form.key
+ ? await fetch(`/api/sections/${form.key}`, {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload),
+ })
+ : await fetch("/api/sections", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload),
+ });
+ if (!res.ok) {
+ setError(formatMessage(dict.sectionManager.saveError, { message: String(res.status) }));
return;
}
setForm(null);
@@ -102,9 +92,9 @@ export default function SectionManager() {
)
)
return;
- const { error } = await supabase.from("sections").delete().eq("key", section.key);
- if (error)
- setError(formatMessage(dict.sectionManager.deleteError, { message: error.message }));
+ const res = await fetch(`/api/sections/${section.key}`, { method: "DELETE" });
+ if (!res.ok)
+ setError(formatMessage(dict.sectionManager.deleteError, { message: String(res.status) }));
await load();
}
@@ -115,12 +105,19 @@ export default function SectionManager() {
setBusy(true);
setError(null);
const [r1, r2] = await Promise.all([
- supabase.from("sections").update({ sort: neighbor.sort }).eq("key", section.key),
- supabase.from("sections").update({ sort: section.sort }).eq("key", neighbor.key),
+ fetch(`/api/sections/${section.key}`, {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ sort: neighbor.sort }),
+ }),
+ fetch(`/api/sections/${neighbor.key}`, {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ sort: section.sort }),
+ }),
]);
- const dbError = r1.error ?? r2.error;
- if (dbError)
- setError(formatMessage(dict.sectionManager.saveError, { message: dbError.message }));
+ if (!r1.ok || !r2.ok)
+ setError(formatMessage(dict.sectionManager.saveError, { message: "move failed" }));
setBusy(false);
await load();
}
diff --git a/components/SuggestionForm.tsx b/components/SuggestionForm.tsx
index 390f0b3..34783a9 100644
--- a/components/SuggestionForm.tsx
+++ b/components/SuggestionForm.tsx
@@ -2,13 +2,11 @@
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("");
@@ -34,23 +32,20 @@ export default function SuggestionForm() {
}
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,
+ const res = await fetch("/api/dish-suggestions", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ name: trimmedName, description: description.trim() || null }),
});
- if (insertError) {
- setError(formatMessage(dict.suggestions.genericError, { message: insertError.message }));
+ 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;
}
diff --git a/components/SuggestionReview.tsx b/components/SuggestionReview.tsx
index 17fa909..b036279 100644
--- a/components/SuggestionReview.tsx
+++ b/components/SuggestionReview.tsx
@@ -1,13 +1,11 @@
"use client";
-import { useCallback, useEffect, useMemo, useState } from "react";
-import { createClient } from "@/lib/supabase/client";
+import { useCallback, useEffect, useState } from "react";
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);
@@ -16,15 +14,12 @@ export default function SuggestionReview() {
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[]);
+ const res = await fetch("/api/dish-suggestions");
+ if (!res.ok)
+ setError(formatMessage(dict.suggestions.loadError, { message: String(res.status) }));
+ else setItems(await res.json());
setLoading(false);
- }, [supabase, dict]);
+ }, [dict]);
useEffect(() => {
load();
@@ -34,25 +29,9 @@ export default function SuggestionReview() {
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 })
- );
+ const res = await fetch(`/api/dish-suggestions/${item.id}/accept`, { method: "POST" });
+ if (!res.ok) {
+ setError(formatMessage(dict.suggestions.actionError, { message: String(res.status) }));
setBusyId(null);
return;
}
@@ -67,9 +46,9 @@ export default function SuggestionReview() {
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 }));
+ const res = await fetch(`/api/dish-suggestions/${item.id}`, { method: "DELETE" });
+ if (!res.ok) {
+ setError(formatMessage(dict.suggestions.actionError, { message: String(res.status) }));
} else {
setInfo(dict.suggestions.rejected);
}
diff --git a/components/UserManager.tsx b/components/UserManager.tsx
index deeaf86..aac87b6 100644
--- a/components/UserManager.tsx
+++ b/components/UserManager.tsx
@@ -1,7 +1,6 @@
"use client";
-import { useCallback, useEffect, useMemo, useState } from "react";
-import { createClient } from "@/lib/supabase/client";
+import { useCallback, useEffect, useState } from "react";
import { formatMessage } from "@/lib/i18n/shared";
import type { Profile, Role } from "@/lib/types";
import { useDict } from "@/components/i18n/LocaleProvider";
@@ -24,30 +23,23 @@ const EMPTY_FORM: UserForm = {
passwordRepeat: "",
};
-export default function UserManager() {
- const supabase = useMemo(() => createClient(), []);
+export default function UserManager({ meId }: { meId: string }) {
const dict = useDict();
const roleLabels: Record = {
admin: dict.common.roleAdmin,
staff: dict.common.roleStaff,
};
const [profiles, setProfiles] = useState([]);
- const [me, setMe] = useState(null);
const [form, setForm] = useState(null);
const [error, setError] = useState(null);
const [busy, setBusy] = useState(false);
const load = useCallback(async () => {
- const [{ data: profileData, error: profileError }, { data: userData }] =
- await Promise.all([
- supabase.from("profiles").select("*").order("created_at"),
- supabase.auth.getUser(),
- ]);
- if (profileError)
- setError(formatMessage(dict.userManager.loadError, { message: profileError.message }));
- else setProfiles(profileData as Profile[]);
- setMe(userData.user?.id ?? null);
- }, [supabase, dict]);
+ const res = await fetch("/api/admin/users");
+ if (!res.ok)
+ setError(formatMessage(dict.userManager.loadError, { message: String(res.status) }));
+ else setProfiles(await res.json());
+ }, [dict]);
useEffect(() => {
load();
@@ -235,7 +227,7 @@ export default function UserManager() {
– {p.email} – {roleLabels[p.role]}
- {p.id === me && {dict.common.you}}
+ {p.id === meId && {dict.common.you}}
- {p.id !== me && (
+ {p.id !== meId && (