From e6077d6182a2062eb90dc200caa913786d8ea642 Mon Sep 17 00:00:00 2001 From: Sabda Yagra Date: Tue, 23 Sep 2025 23:49:33 +0700 Subject: [PATCH] feat: category table and create --- .../admin/categories/components/columns.tsx | 186 +++++++++++ .../components/table-categories.tsx | 274 +++++++++++++++ app/(admin)/admin/categories/create/page.tsx | 13 + app/(admin)/admin/categories/layout.tsx | 11 + app/(admin)/admin/categories/page.tsx | 315 ++++++++++++++++++ .../landing-page/retracting-sidedar.tsx | 27 +- service/categories/categories.ts | 90 +++++ 7 files changed, 909 insertions(+), 7 deletions(-) create mode 100644 app/(admin)/admin/categories/components/columns.tsx create mode 100644 app/(admin)/admin/categories/components/table-categories.tsx create mode 100644 app/(admin)/admin/categories/create/page.tsx create mode 100644 app/(admin)/admin/categories/layout.tsx create mode 100644 app/(admin)/admin/categories/page.tsx create mode 100644 service/categories/categories.ts diff --git a/app/(admin)/admin/categories/components/columns.tsx b/app/(admin)/admin/categories/components/columns.tsx new file mode 100644 index 0000000..3dc8954 --- /dev/null +++ b/app/(admin)/admin/categories/components/columns.tsx @@ -0,0 +1,186 @@ +"use client"; +import * as React from "react"; +import { ColumnDef } from "@tanstack/react-table"; +import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react"; +import { cn, getCookiesDecrypt } from "@/lib/utils"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuTrigger, + DropdownMenuItem, +} from "@/components/ui/dropdown-menu"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { format } from "date-fns"; +import { useRouter } from "next/navigation"; +import Swal from "sweetalert2"; +import withReactContent from "sweetalert2-react-content"; +import Link from "next/link"; +import { error } from "@/lib/swal"; +import { deleteCategories } from "@/service/categories/categories"; + +const useCategoryColumns = () => { + const MySwal = withReactContent(Swal); + + const columns: ColumnDef[] = [ + { + accessorKey: "no", + header: "No", + cell: ({ row }) => {row.getValue("no")}, + }, + { + accessorKey: "title", + header: "Title", + cell: ({ row }) => { + const title: string = row.getValue("title"); + return ( + + {title?.length > 50 ? `${title.slice(0, 30)}...` : title} + + ); + }, + }, + { + accessorKey: "description", + header: "Description", + cell: ({ row }) => { + const desc: string = row.getValue("description"); + return ( + + {desc?.length > 80 ? `${desc.slice(0, 50)}...` : desc || "-"} + + ); + }, + }, + { + accessorKey: "createdAt", + header: "Created At", + cell: ({ row }) => { + const createdAt = row.getValue("createdAt") as string | number; + return createdAt ? ( + + {format(new Date(createdAt), "dd-MM-yyyy HH:mm")} + + ) : ( + "-" + ); + }, + }, + { + accessorKey: "createdByName", + header: "Created By", + cell: ({ row }) => {row.getValue("createdByName") || "-"}, + }, + { + accessorKey: "isPublish", + header: "Publish", + cell: ({ row }) => { + const isPublish = row.getValue("isPublish"); + return ( + + {isPublish ? "Published" : "Draft"} + + ); + }, + }, + // { + // accessorKey: "statusName", + // header: "Status", + // cell: ({ row }) => { + // const statusName = row.getValue("statusName"); + // return ( + // + // {statusName || "-"} + // + // ); + // }, + // }, + { + id: "actions", + header: "Action", + cell: ({ row }) => { + const router = useRouter(); + const MySwal = withReactContent(Swal); + + async function doDelete(id: number) { + const response = await deleteCategories(id); + if (response?.error) { + error(response.message); + return; + } + MySwal.fire({ + title: "Sukses", + icon: "success", + confirmButtonText: "OK", + }).then(() => { + window.location.reload(); + }); + } + + const handleDelete = (id: number) => { + MySwal.fire({ + title: "Hapus Category?", + icon: "warning", + showCancelButton: true, + confirmButtonColor: "#d33", + confirmButtonText: "Hapus", + }).then((result) => { + if (result.isConfirmed) doDelete(id); + }); + }; + + return ( + + + + + + + + View + + + + + Edit + + + handleDelete(row.original.id)} + className="text-destructive focus:bg-destructive focus:text-white" + > + Delete + + + + ); + }, + }, + ]; + + return columns; +}; + +export default useCategoryColumns; diff --git a/app/(admin)/admin/categories/components/table-categories.tsx b/app/(admin)/admin/categories/components/table-categories.tsx new file mode 100644 index 0000000..72ad8d4 --- /dev/null +++ b/app/(admin)/admin/categories/components/table-categories.tsx @@ -0,0 +1,274 @@ +"use client"; + +import * as React from "react"; +import { + ColumnDef, + flexRender, + getCoreRowModel, + getPaginationRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { Button } from "@/components/ui/button"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + ChevronDown, + Eye, + MoreVertical, + SquarePen, + Trash2, +} from "lucide-react"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, + DropdownMenuItem, +} from "@/components/ui/dropdown-menu"; + +import TablePagination from "@/components/table/table-pagination"; +import { + deleteCategories, + getCategories, +} from "@/service/categories/categories"; +import Swal from "sweetalert2"; +import withReactContent from "sweetalert2-react-content"; +import Link from "next/link"; +import { error } from "@/lib/swal"; + +const TableCategories = () => { + const [dataTable, setDataTable] = React.useState([]); + const [totalData, setTotalData] = React.useState(1); + const [totalPage, setTotalPage] = React.useState(1); + const [page, setPage] = React.useState(1); + const [showData, setShowData] = React.useState("10"); + + const MySwal = withReactContent(Swal); + + // ✅ definisi kolom untuk table categories + const columns: ColumnDef[] = [ + { + accessorKey: "no", + header: "No", + cell: ({ row }) => row.original.no, + }, + { + accessorKey: "title", + header: "Nama Kategori", + cell: ({ row }) => row.original.title || "-", + }, + { + accessorKey: "description", + header: "Deskripsi", + cell: ({ row }) => row.original.description || "-", + }, + { + accessorKey: "statusId", + header: "Status", + cell: ({ row }) => (row.original.isPublish ? "Publish" : "Draft"), + }, + { + id: "actions", + header: "Action", + enableHiding: false, + cell: ({ row }) => { + const doDelete = async (id: number) => { + const response = await deleteCategories(id); + if (response?.error) { + error(response.message); + return; + } + MySwal.fire({ + title: "Sukses", + icon: "success", + confirmButtonText: "OK", + }).then(() => { + window.location.reload(); + }); + }; + + const handleDelete = (id: number) => { + MySwal.fire({ + title: "Hapus Kategori?", + icon: "warning", + showCancelButton: true, + cancelButtonColor: "#3085d6", + confirmButtonColor: "#d33", + confirmButtonText: "Hapus", + }).then((result) => { + if (result.isConfirmed) { + doDelete(id); + } + }); + }; + + return ( + + + + + + + + View + + + + + Edit + + + handleDelete(row.original.id)} + className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-white rounded-none" + > + Delete + + + + ); + }, + }, + ]; + + // setup table + const table = useReactTable({ + data: dataTable, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + }); + + // fetch categories data + React.useEffect(() => { + fetchData(); + }, [page, showData]); + + async function fetchData() { + try { + const res = await getCategories({ + page, + limit: Number(showData), + sort: "desc", + sortBy: "id", + }); + + const data = res?.data?.data?.content || res?.data?.data || []; + const count = res?.data?.data?.totalElements || data.length; + const totalPages = + res?.data?.data?.totalPages || Math.ceil(count / Number(showData)); + + // tambahkan nomor urut + const withIndex = data.map((item: any, index: number) => ({ + ...item, + no: (page - 1) * Number(showData) + index + 1, + })); + + setDataTable(withIndex); + setTotalData(count); + setTotalPage(totalPages); + } catch (err) { + console.error("Error fetch categories:", err); + setDataTable([]); + } + } + + return ( + <> +
+
+

Daftar Kategori

+ + + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ))} + + +
+ + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {dataTable.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No categories found. + + + )} + +
+ + +
+ + ); +}; + +export default TableCategories; diff --git a/app/(admin)/admin/categories/create/page.tsx b/app/(admin)/admin/categories/create/page.tsx new file mode 100644 index 0000000..7883c57 --- /dev/null +++ b/app/(admin)/admin/categories/create/page.tsx @@ -0,0 +1,13 @@ + +const ImageCreatePage = async () => { + return ( +
+ {/* */} +
+ {/* */} +
+
+ ); +}; + +export default ImageCreatePage; diff --git a/app/(admin)/admin/categories/layout.tsx b/app/(admin)/admin/categories/layout.tsx new file mode 100644 index 0000000..6e95670 --- /dev/null +++ b/app/(admin)/admin/categories/layout.tsx @@ -0,0 +1,11 @@ +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Media Hub | POLRI", + description: "Media Hub merupakan situs resmi milik Divisi Humas Polri di mana di dalamnya berisi konten-konten yang dapat diakses secara gratis oleh Internal Polri, Jurnalis, Masyarakat Umum, dan KSP.", +}; +const Layout = ({ children }: { children: React.ReactNode }) => { + return <>{children}; +}; + +export default Layout; diff --git a/app/(admin)/admin/categories/page.tsx b/app/(admin)/admin/categories/page.tsx new file mode 100644 index 0000000..4bcd4df --- /dev/null +++ b/app/(admin)/admin/categories/page.tsx @@ -0,0 +1,315 @@ +"use client"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import SiteBreadcrumb from "@/components/site-breadcrumb"; +import { UploadIcon } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import Link from "next/link"; +import TableCategories from "./components/table-categories"; +import { z } from "zod"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { loading, close, error } from "@/config/swal"; +import { useState } from "react"; +import withReactContent from "sweetalert2-react-content"; +import Swal from "sweetalert2"; +import { createCategories } from "@/service/categories/categories"; + +const FormSchema = z.object({ + createdById: z.coerce.number(), + description: z.string().min(1, "Deskripsi wajib diisi"), + oldCategoryId: z.coerce.number(), + parentId: z.coerce.number(), + slug: z.string().min(1, "Slug wajib diisi"), + statusId: z.coerce.number(), + tags: z.string().min(1, "Tags wajib diisi"), + title: z.string().min(1, "Nama kategori wajib diisi"), +}); + +// helper untuk bikin slug otomatis +const slugify = (text: string) => + text + .toLowerCase() + .trim() + .replace(/[^a-z0-9\s-]/g, "") + .replace(/\s+/g, "-") + .replace(/-+/g, "-"); + +const ReactTableImagePage = () => { + const [isOpen, setIsOpen] = useState(false); + const MySwal = withReactContent(Swal); + + const form = useForm>({ + resolver: zodResolver(FormSchema), + defaultValues: { + createdById: 1, + description: "", + oldCategoryId: 0, + parentId: 0, + slug: "", + statusId: 1, + tags: "", + title: "", + }, + }); + + // Auto generate slug ketika title berubah + const handleTitleChange = (value: string) => { + form.setValue("title", value); + const autoSlug = slugify(value); + if ( + !form.getValues("slug") || + form.getValues("slug") === + slugify(form.formState.defaultValues?.title || "") + ) { + form.setValue("slug", autoSlug); + } + }; + + const onSubmit = async (data: z.infer) => { + loading(); + try { + const response = await createCategories(data); + + // ✅ Tutup swal "Loading" sebelum tampil swal baru + MySwal.close(); + + if (response?.error) { + const message = + typeof response.message === "string" + ? response.message + : Array.isArray(response.message) + ? response.message.join(", ") + : JSON.stringify(response.message); + + await MySwal.fire({ + icon: "error", + title: "Gagal", + text: message || "Gagal membuat kategori", + confirmButtonColor: "#d33", + timer: 2000, // ⏱ otomatis hilang setelah 2 detik + showConfirmButton: false, + }); + return; + } + + await MySwal.fire({ + icon: "success", + title: "Sukses", + text: "Kategori berhasil dibuat", + confirmButtonColor: "#3085d6", + timer: 2000, // ⏱ otomatis hilang setelah 2 detik + showConfirmButton: false, + }); + + setIsOpen(false); + } catch (err: any) { + MySwal.close(); + + await MySwal.fire({ + icon: "error", + title: "Terjadi kesalahan", + text: err?.message || "Coba lagi nanti", + confirmButtonColor: "#d33", + timer: 2000, // ⏱ otomatis hilang setelah 2 detik + showConfirmButton: false, + }); + } + }; + + return ( +
+ {/* */} +
+
+ + + +
+
+
+ +
+
+

+ Categories Management +

+

+ Manage your categories +

+
+
+
+ + + + + + + Buat Kategori Baru + +
+ + {/* Title */} + ( + + Nama Kategori + + + handleTitleChange(e.target.value) + } + /> + + + + )} + /> + + {/* Slug */} + ( + + Slug + + + + + + )} + /> + + {/* Deskripsi */} + ( + + Deskripsi + +