From a7bea17f3dfbeb316cee16aea7483a9b19a7192b Mon Sep 17 00:00:00 2001
From: Zekbot001 <280472700+Zekbot001@users.noreply.github.com>
Date: Sun, 31 May 2026 20:25:57 +0200
Subject: [PATCH 1/2] fix(directory): keep failed owner actions in place
---
.../[id]/DirectoryOwnerActions.test.tsx | 62 +++++++++++++++++++
.../directory/[id]/DirectoryOwnerActions.tsx | 60 ++++++++++++++----
2 files changed, 111 insertions(+), 11 deletions(-)
create mode 100644 src/app/directory/[id]/DirectoryOwnerActions.test.tsx
diff --git a/src/app/directory/[id]/DirectoryOwnerActions.test.tsx b/src/app/directory/[id]/DirectoryOwnerActions.test.tsx
new file mode 100644
index 00000000..bd92a67d
--- /dev/null
+++ b/src/app/directory/[id]/DirectoryOwnerActions.test.tsx
@@ -0,0 +1,62 @@
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { DirectoryOwnerActions } from "./DirectoryOwnerActions";
+
+const { push, refresh } = vi.hoisted(() => ({
+ push: vi.fn(),
+ refresh: vi.fn(),
+}));
+
+vi.mock("next/navigation", () => ({
+ useRouter: () => ({ push, refresh }),
+}));
+
+const listing = {
+ id: "listing-1",
+ title: "Example",
+ url: "https://example.com",
+ description: null,
+ tags: [],
+ logo_url: null,
+ banner_url: null,
+ screenshot_url: null,
+ status: "active",
+};
+
+describe("DirectoryOwnerActions", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.stubGlobal("fetch", vi.fn());
+ vi.stubGlobal("confirm", vi.fn(() => true));
+ });
+
+ afterEach(() => {
+ cleanup();
+ vi.unstubAllGlobals();
+ });
+
+ it("keeps the listing in place when a visibility update fails", async () => {
+ vi.mocked(fetch).mockResolvedValue(
+ new Response(JSON.stringify({ error: "Visibility denied" }), { status: 403 })
+ );
+
+ render();
+ await userEvent.click(screen.getByRole("button", { name: "Hide" }));
+
+ expect(await screen.findByRole("alert")).toHaveTextContent("Visibility denied");
+ expect(refresh).not.toHaveBeenCalled();
+ });
+
+ it("keeps the listing in place when deletion fails", async () => {
+ vi.mocked(fetch).mockResolvedValue(
+ new Response(JSON.stringify({ error: "Delete denied" }), { status: 403 })
+ );
+
+ render();
+ await userEvent.click(screen.getByRole("button", { name: "Delete" }));
+
+ expect(await screen.findByRole("alert")).toHaveTextContent("Delete denied");
+ expect(push).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/app/directory/[id]/DirectoryOwnerActions.tsx b/src/app/directory/[id]/DirectoryOwnerActions.tsx
index eaddcc9c..46e9a3b2 100644
--- a/src/app/directory/[id]/DirectoryOwnerActions.tsx
+++ b/src/app/directory/[id]/DirectoryOwnerActions.tsx
@@ -22,6 +22,15 @@ interface DirectoryOwnerActionsProps {
};
}
+async function responseError(res: Response, fallback: string) {
+ try {
+ const data = await res.json();
+ return data.error || fallback;
+ } catch {
+ return fallback;
+ }
+}
+
export function DirectoryOwnerActions({ listing }: DirectoryOwnerActionsProps) {
const router = useRouter();
const [editing, setEditing] = useState(false);
@@ -72,22 +81,46 @@ export function DirectoryOwnerActions({ listing }: DirectoryOwnerActionsProps) {
}
async function handleToggleVisibility() {
+ setError("");
setLoading(true);
- const newStatus = listing.status === "active" ? "hidden" : "active";
- await fetch(`/api/directory/${listing.id}`, {
- method: "PATCH",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ status: newStatus }),
- });
- setLoading(false);
- router.refresh();
+ try {
+ const newStatus = listing.status === "active" ? "hidden" : "active";
+ const res = await fetch(`/api/directory/${listing.id}`, {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ status: newStatus }),
+ });
+
+ if (!res.ok) {
+ setError(await responseError(res, "Visibility update failed"));
+ return;
+ }
+
+ router.refresh();
+ } catch {
+ setError("Visibility update failed");
+ } finally {
+ setLoading(false);
+ }
}
async function handleDelete() {
if (!confirm("Delete this listing? This cannot be undone and there is no refund.")) return;
+ setError("");
setLoading(true);
- await fetch(`/api/directory/${listing.id}`, { method: "DELETE" });
- router.push("/directory");
+ try {
+ const res = await fetch(`/api/directory/${listing.id}`, { method: "DELETE" });
+ if (!res.ok) {
+ setError(await responseError(res, "Delete failed"));
+ return;
+ }
+
+ router.push("/directory");
+ } catch {
+ setError("Delete failed");
+ } finally {
+ setLoading(false);
+ }
}
if (editing) {
@@ -95,7 +128,7 @@ export function DirectoryOwnerActions({ listing }: DirectoryOwnerActionsProps) {
Edit Listing
{error && (
-
+
{error}
)}
@@ -140,6 +173,11 @@ export function DirectoryOwnerActions({ listing }: DirectoryOwnerActionsProps) {
return (
Owner Actions
+ {error && (
+
+ {error}
+
+ )}
-
+
@@ -179,7 +182,7 @@ export function DirectoryOwnerActions({ listing }: DirectoryOwnerActionsProps) {
)}
-