Skip to content

Commit 7ddd023

Browse files
committed
feat(categories): implement delete category functionality and add confirmation modal
1 parent fecbb59 commit 7ddd023

File tree

5 files changed

+143
-10
lines changed

5 files changed

+143
-10
lines changed

src/actions/categories.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,16 @@ export const updateCategory = async (id: string, input: CategoryForm) => {
3434
if (error) throw new Error(`Error updating category: ${error.message}`);
3535
return data;
3636
};
37+
38+
export async function deleteCategory(categoryId: string) {
39+
const supabase = await createClient();
40+
const { error } = await supabase
41+
.from("categories")
42+
.delete()
43+
.eq("id", categoryId);
44+
45+
if (error) {
46+
console.error("Error deleting category:", error);
47+
throw new Error("Failed to delete category");
48+
}
49+
}

src/app/(private)/dashboard/categories/client.tsx

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@ import { useRouter } from "next/navigation";
77
import { useDisclosure, addToast } from "@heroui/react";
88
import { useForm } from "react-hook-form";
99
import { zodResolver } from "@hookform/resolvers/zod";
10-
import { createCategory, updateCategory } from "@/actions/categories";
10+
import {
11+
createCategory,
12+
deleteCategory,
13+
updateCategory,
14+
} from "@/actions/categories";
1115
import { CategoryDrawer, CategoryTable } from "@/components/categories";
12-
import { HeadingDashboard } from "@/components/ui";
16+
import { DeleteModal, HeadingDashboard } from "@/components/ui";
1317
import { categorySchema } from "@/lib/zod";
1418

1519
type Props = {
@@ -18,10 +22,17 @@ type Props = {
1822

1923
export default function CategoriesPageClient({ categories }: Props) {
2024
const router = useRouter();
21-
const { isOpen, onOpen, onClose } = useDisclosure();
2225
const [editingCategory, setEditingCategory] = useState<Category | null>(null);
26+
const [categoryToDelete, setCategoryToDelete] = useState<string | null>(null);
2327
const [loading, setLoading] = useState<boolean>(false);
2428

29+
const { isOpen, onOpen, onClose } = useDisclosure();
30+
const {
31+
isOpen: isConfirmOpen,
32+
onOpen: onConfirmOpen,
33+
onClose: onConfirmClose,
34+
} = useDisclosure();
35+
2536
const {
2637
register,
2738
handleSubmit,
@@ -48,10 +59,8 @@ export default function CategoriesPageClient({ categories }: Props) {
4859
setLoading(true);
4960
try {
5061
const payload = { name: data.name };
51-
52-
editingCategory
53-
? await updateCategory(editingCategory.id, payload)
54-
: await createCategory(payload);
62+
if (editingCategory) await updateCategory(editingCategory.id, payload);
63+
else await createCategory(payload);
5564

5665
router.refresh();
5766
setEditingCategory(null);
@@ -64,14 +73,48 @@ export default function CategoriesPageClient({ categories }: Props) {
6473
}
6574
};
6675

76+
const handleDelete = (id: string) => {
77+
setCategoryToDelete(id);
78+
onConfirmOpen();
79+
};
80+
81+
const confirmDelete = async () => {
82+
if (!categoryToDelete) return;
83+
setLoading(true);
84+
try {
85+
await deleteCategory(categoryToDelete);
86+
addToast({
87+
title: "Deleted",
88+
description: "Category deleted successfully.",
89+
});
90+
router.refresh();
91+
} catch (error) {
92+
console.error("Delete error:", error);
93+
addToast({ title: "Error", description: "Failed to delete category." });
94+
} finally {
95+
setCategoryToDelete(null);
96+
setLoading(false);
97+
onConfirmClose();
98+
}
99+
};
100+
67101
return (
68102
<div className="flex flex-col gap-4">
103+
{/* Heading */}
69104
<HeadingDashboard
70105
title="Categories"
71106
count={categories.length}
72107
openAddModal={openAddModal}
73108
/>
74-
<CategoryTable categories={categories} openEditModal={openEditModal} />
109+
110+
{/* Table */}
111+
<CategoryTable
112+
categories={categories}
113+
openEditModal={openEditModal}
114+
handleDelete={handleDelete}
115+
/>
116+
117+
{/* Drawer */}
75118
<CategoryDrawer
76119
editingCategory={editingCategory}
77120
errors={errors}
@@ -83,6 +126,17 @@ export default function CategoriesPageClient({ categories }: Props) {
83126
loading={loading}
84127
isSubmitting={isSubmitting}
85128
/>
129+
130+
{/* Delete Modal */}
131+
<DeleteModal
132+
loading={loading}
133+
isConfirmOpen={isConfirmOpen}
134+
onConfirmClose={onConfirmClose}
135+
confirmDelete={confirmDelete}
136+
title="Delete Category"
137+
description="Are you sure you want to delete this category? This action cannot be undone."
138+
confirmLabel="Delete"
139+
/>
86140
</div>
87141
);
88142
}

src/components/categories/category-table.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,14 @@ import { EllipsisVertical, SquarePenIcon, Trash2 } from "lucide-react";
2020
type Props = {
2121
categories: Category[];
2222
openEditModal: (category: Category) => void;
23+
handleDelete: (id: string) => void;
2324
};
2425

25-
export default function CategoryTable({ categories, openEditModal }: Props) {
26+
export default function CategoryTable({
27+
categories,
28+
openEditModal,
29+
handleDelete,
30+
}: Props) {
2631
return (
2732
<Table
2833
removeWrapper
@@ -77,7 +82,7 @@ export default function CategoryTable({ categories, openEditModal }: Props) {
7782
color="danger"
7883
shortcut="⌘D"
7984
startContent={<Trash2 size={18} />}
80-
// onPress={() => handleDeleteClick(resource.id)}
85+
onPress={() => handleDelete(category.id)}
8186
>
8287
Delete category
8388
</DropdownItem>

src/components/ui/delete-modal.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"use client";
2+
3+
import {
4+
Button,
5+
Modal,
6+
ModalBody,
7+
ModalContent,
8+
ModalFooter,
9+
ModalHeader,
10+
} from "@heroui/react";
11+
12+
type Props = {
13+
loading: boolean;
14+
isConfirmOpen: boolean;
15+
onConfirmClose: () => void;
16+
confirmDelete: () => void;
17+
title?: string;
18+
description?: string;
19+
confirmLabel?: string;
20+
cancelLabel?: string;
21+
};
22+
23+
export default function DeleteModal({
24+
loading,
25+
isConfirmOpen,
26+
onConfirmClose,
27+
confirmDelete,
28+
title = "Confirm Deletion",
29+
description = "Are you sure you want to proceed? This action cannot be undone.",
30+
confirmLabel = "Confirm",
31+
cancelLabel = "Cancel",
32+
}: Props) {
33+
return (
34+
<Modal
35+
backdrop="blur"
36+
isOpen={isConfirmOpen}
37+
onClose={onConfirmClose}
38+
classNames={{
39+
base: "bg-white dark:bg-neutral-950 border-2 border-neutral-200 dark:border-neutral-800",
40+
}}
41+
>
42+
<ModalContent>
43+
<ModalHeader>{title}</ModalHeader>
44+
<ModalBody>{description}</ModalBody>
45+
<ModalFooter>
46+
<Button
47+
variant="bordered"
48+
onPress={onConfirmClose}
49+
className="border-2 border-neutral-200 dark:border-neutral-800"
50+
>
51+
{cancelLabel}
52+
</Button>
53+
<Button color="danger" onPress={confirmDelete} isLoading={loading}>
54+
{confirmLabel}
55+
</Button>
56+
</ModalFooter>
57+
</ModalContent>
58+
</Modal>
59+
);
60+
}

src/components/ui/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export { default as BackToTop } from "./back-to-top";
22
export { default as Breadcrumbs } from "./breadcrumbs";
3+
export { default as DeleteModal } from "./delete-modal";
34
export { default as Heading } from "./heading";
45
export { default as HeadingDashboard } from "./heading-dashboard";
56
export { default as ThemeSwitcher } from "./theme-switcher";

0 commit comments

Comments
 (0)