feat: category table and create
This commit is contained in:
parent
50b1b1fed9
commit
e6077d6182
|
|
@ -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<any>[] = [
|
||||
{
|
||||
accessorKey: "no",
|
||||
header: "No",
|
||||
cell: ({ row }) => <span>{row.getValue("no")}</span>,
|
||||
},
|
||||
{
|
||||
accessorKey: "title",
|
||||
header: "Title",
|
||||
cell: ({ row }) => {
|
||||
const title: string = row.getValue("title");
|
||||
return (
|
||||
<span className="whitespace-nowrap">
|
||||
{title?.length > 50 ? `${title.slice(0, 30)}...` : title}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "description",
|
||||
header: "Description",
|
||||
cell: ({ row }) => {
|
||||
const desc: string = row.getValue("description");
|
||||
return (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{desc?.length > 80 ? `${desc.slice(0, 50)}...` : desc || "-"}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
header: "Created At",
|
||||
cell: ({ row }) => {
|
||||
const createdAt = row.getValue("createdAt") as string | number;
|
||||
return createdAt ? (
|
||||
<span className="whitespace-nowrap">
|
||||
{format(new Date(createdAt), "dd-MM-yyyy HH:mm")}
|
||||
</span>
|
||||
) : (
|
||||
"-"
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "createdByName",
|
||||
header: "Created By",
|
||||
cell: ({ row }) => <span>{row.getValue("createdByName") || "-"}</span>,
|
||||
},
|
||||
{
|
||||
accessorKey: "isPublish",
|
||||
header: "Publish",
|
||||
cell: ({ row }) => {
|
||||
const isPublish = row.getValue("isPublish");
|
||||
return (
|
||||
<Badge
|
||||
className={cn(
|
||||
"rounded-full px-3",
|
||||
isPublish
|
||||
? "bg-green-100 text-green-600"
|
||||
: "bg-red-100 text-red-600"
|
||||
)}
|
||||
>
|
||||
{isPublish ? "Published" : "Draft"}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
// {
|
||||
// accessorKey: "statusName",
|
||||
// header: "Status",
|
||||
// cell: ({ row }) => {
|
||||
// const statusName = row.getValue("statusName");
|
||||
// return (
|
||||
// <Badge
|
||||
// className={cn(
|
||||
// "rounded-full px-3",
|
||||
// statusName === "Active"
|
||||
// ? "bg-green-100 text-green-600"
|
||||
// : "bg-gray-200 text-gray-600"
|
||||
// )}
|
||||
// >
|
||||
// {statusName || "-"}
|
||||
// </Badge>
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
{
|
||||
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 (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="icon" variant="ghost">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<Link
|
||||
href={`/admin/categories/detail/${row.original.id}`}
|
||||
className="hover:text-black"
|
||||
>
|
||||
<DropdownMenuItem>
|
||||
<Eye className="w-4 h-4 mr-2" /> View
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<Link
|
||||
href={`/admin/categories/update/${row.original.id}`}
|
||||
className="hover:text-black"
|
||||
>
|
||||
<DropdownMenuItem>
|
||||
<SquarePen className="w-4 h-4 mr-2" /> Edit
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDelete(row.original.id)}
|
||||
className="text-destructive focus:bg-destructive focus:text-white"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" /> Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return columns;
|
||||
};
|
||||
|
||||
export default useCategoryColumns;
|
||||
|
|
@ -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<any[]>([]);
|
||||
const [totalData, setTotalData] = React.useState<number>(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<any>[] = [
|
||||
{
|
||||
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 (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="bg-transparent hover:bg-transparent"
|
||||
>
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreVertical className="h-4 w-4 text-default-800" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="p-0" align="end">
|
||||
<Link href={`/admin/categories/detail/${row.original.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 rounded-none">
|
||||
<Eye className="w-4 h-4 mr-1.5" /> View
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<Link href={`/admin/categories/update/${row.original.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 rounded-none">
|
||||
<SquarePen className="w-4 h-4 mr-1.5" /> Edit
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDelete(row.original.id)}
|
||||
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-white rounded-none"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-1.5" /> Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// 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 (
|
||||
<>
|
||||
<div className="w-full overflow-x-auto bg-white rounded-lg border border-gray-200 shadow-sm">
|
||||
<div className="flex justify-between items-center px-4 py-2">
|
||||
<h2 className="text-lg font-semibold">Daftar Kategori</h2>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="ml-auto" size="sm">
|
||||
Columns <ChevronDown />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{table
|
||||
.getAllColumns()
|
||||
.filter((column) => column.getCanHide())
|
||||
.map((column) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={column.id}
|
||||
className="capitalize"
|
||||
checked={column.getIsVisible()}
|
||||
onCheckedChange={(value) =>
|
||||
column.toggleVisibility(!!value)
|
||||
}
|
||||
>
|
||||
{column.id}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id} className="bg-default-200">
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{dataTable.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
No categories found.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<TablePagination
|
||||
table={table}
|
||||
totalData={totalData}
|
||||
totalPage={totalPage}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableCategories;
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
|
||||
const ImageCreatePage = async () => {
|
||||
return (
|
||||
<div>
|
||||
{/* <SiteBreadcrumb /> */}
|
||||
<div className="space-y-4">
|
||||
{/* <CategoriesForm /> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageCreatePage;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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<z.infer<typeof FormSchema>>({
|
||||
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<typeof FormSchema>) => {
|
||||
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 (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* <SiteBreadcrumb /> */}
|
||||
<div className="p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<Card className="shadow-sm border-0">
|
||||
<CardHeader className="border-b border-gray-200 bg-white rounded-t-lg">
|
||||
<CardTitle>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<UploadIcon className="w-4 h-4 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-gray-900">
|
||||
Categories Management
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500">
|
||||
Manage your categories
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-none">
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button color="primary">Tambah Kategori</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Buat Kategori Baru</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-4"
|
||||
>
|
||||
{/* Title */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Nama Kategori</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Masukkan nama kategori"
|
||||
{...field}
|
||||
onChange={(e) =>
|
||||
handleTitleChange(e.target.value)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Slug */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="slug"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Slug</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="contoh: berita-politik"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Deskripsi */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Deskripsi</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Masukkan deskripsi kategori"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Tags */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="tags"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Tags</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="contoh: berita, politik"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* createdById, oldCategoryId, parentId, statusId → bisa hidden atau number input */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="createdById"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Created By ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="statusId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Status ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="submit" color="primary">
|
||||
Simpan
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{/* <Link href={"/admin/categories/create"}>
|
||||
<Button
|
||||
color="primary"
|
||||
className="text-white shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<UploadIcon size={18} className="mr-2" />
|
||||
Create Categories
|
||||
</Button>
|
||||
</Link> */}
|
||||
{/* <Link href={"/contributor/content/image/createAi"}>
|
||||
<Button color="primary" className="text-white ml-3">
|
||||
<UploadIcon />
|
||||
Unggah Foto Dengan AI
|
||||
</Button>
|
||||
</Link> */}
|
||||
</div>
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6 bg-gray-50">
|
||||
<TableCategories />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReactTableImagePage;
|
||||
|
|
@ -55,10 +55,10 @@ const sidebarSections: SidebarSection[] = [
|
|||
],
|
||||
},
|
||||
{
|
||||
title: "Content",
|
||||
title: "Konten",
|
||||
items: [
|
||||
{
|
||||
title: "Master Data",
|
||||
title: "Konten",
|
||||
icon: () => <Icon icon="mdi:folder-outline" className="text-lg" />,
|
||||
children: [
|
||||
{
|
||||
|
|
@ -90,6 +90,17 @@ const sidebarSections: SidebarSection[] = [
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Master Data",
|
||||
icon: () => <Icon icon="mdi:folder-outline" className="text-lg" />,
|
||||
children: [
|
||||
{
|
||||
title: "Kategori Konten",
|
||||
icon: () => <Icon icon="ri:article-line" className="text-lg" />,
|
||||
link: "/admin/categories",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
@ -207,10 +218,12 @@ const SidebarContent = ({
|
|||
updateSidebarData: (newData: boolean) => void;
|
||||
}) => {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const [expanded, setExpanded] = useState<string | null>(null); // track parent yang dibuka
|
||||
const [expanded, setExpanded] = useState<string[]>([]);
|
||||
|
||||
const toggleExpand = (title: string) => {
|
||||
setExpanded((prev) => (prev === title ? null : title));
|
||||
setExpanded((prev) =>
|
||||
prev.includes(title) ? prev.filter((t) => t !== title) : [...prev, title]
|
||||
);
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
|
|
@ -306,7 +319,7 @@ const SidebarContent = ({
|
|||
{open && (
|
||||
<motion.span
|
||||
animate={{
|
||||
rotate: expanded === item.title ? 90 : 0,
|
||||
rotate: expanded.includes(item.title) ? 90 : 0,
|
||||
}}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="text-slate-500"
|
||||
|
|
@ -320,8 +333,8 @@ const SidebarContent = ({
|
|||
<motion.div
|
||||
initial={false}
|
||||
animate={{
|
||||
height: expanded === item.title ? "auto" : 0,
|
||||
opacity: expanded === item.title ? 1 : 0,
|
||||
height: expanded.includes(item.title) ? "auto" : 0,
|
||||
opacity: expanded.includes(item.title) ? 1 : 0,
|
||||
}}
|
||||
className="overflow-hidden ml-6 space-y-1"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
import { getCookiesDecrypt } from "@/lib/utils";
|
||||
import { httpPost } from "../http-config/http-base-service";
|
||||
import {
|
||||
httpDeleteInterceptor,
|
||||
httpGetInterceptor,
|
||||
httpPostInterceptor,
|
||||
} from "../http-config/http-interceptor-service";
|
||||
|
||||
interface GetCategoriesParams {
|
||||
UserLevelId?: number;
|
||||
UserLevelNumber?: number;
|
||||
description?: string;
|
||||
isPublish?: boolean;
|
||||
parentId?: number;
|
||||
statusId?: number;
|
||||
title?: string;
|
||||
count?: number;
|
||||
limit?: number;
|
||||
nextPage?: number;
|
||||
page?: number;
|
||||
previousPage?: number;
|
||||
sort?: string;
|
||||
sortBy?: string;
|
||||
totalPage?: number;
|
||||
region?: string; // kalau ada publishedLocation
|
||||
}
|
||||
|
||||
interface CreateCategoryPayload {
|
||||
createdById: number;
|
||||
description: string;
|
||||
oldCategoryId: number;
|
||||
parentId: number;
|
||||
slug: string;
|
||||
statusId: number;
|
||||
title: string;
|
||||
tags: string;
|
||||
}
|
||||
|
||||
export async function getCategories(params: GetCategoriesParams) {
|
||||
const query = new URLSearchParams();
|
||||
|
||||
if (params.UserLevelId)
|
||||
query.append("UserLevelId", params.UserLevelId.toString());
|
||||
if (params.UserLevelNumber)
|
||||
query.append("UserLevelNumber", params.UserLevelNumber.toString());
|
||||
if (params.description) query.append("description", params.description);
|
||||
if (params.isPublish !== undefined)
|
||||
query.append("isPublish", String(params.isPublish));
|
||||
if (params.parentId) query.append("parentId", params.parentId.toString());
|
||||
if (params.statusId) query.append("statusId", params.statusId.toString());
|
||||
if (params.title) query.append("title", params.title);
|
||||
if (params.count) query.append("count", params.count.toString());
|
||||
if (params.limit) query.append("limit", params.limit.toString());
|
||||
if (params.nextPage) query.append("nextPage", params.nextPage.toString());
|
||||
if (params.page) query.append("page", params.page.toString());
|
||||
if (params.previousPage)
|
||||
query.append("previousPage", params.previousPage.toString());
|
||||
if (params.sort) query.append("sort", params.sort);
|
||||
if (params.sortBy) query.append("sortBy", params.sortBy);
|
||||
if (params.totalPage) query.append("totalPage", params.totalPage.toString());
|
||||
if (params.region) query.append("publishedLocation", params.region);
|
||||
|
||||
const url = `/article-categories?${query.toString()}`;
|
||||
|
||||
return httpGetInterceptor(url);
|
||||
}
|
||||
|
||||
export async function deleteCategories(id: number) {
|
||||
const clientKey = getCookiesDecrypt("clientKey");
|
||||
const csrfToken = getCookiesDecrypt("csrfToken");
|
||||
const url = `/article-categories/${id}`;
|
||||
|
||||
return httpDeleteInterceptor(url, {
|
||||
headers: {
|
||||
"X-Client-Key": clientKey,
|
||||
"X-Csrf-Token": csrfToken,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function createCategories(data: CreateCategoryPayload) {
|
||||
const url = "/article-categories";
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Client-Key": process.env.NEXT_PUBLIC_CLIENT_KEY || "",
|
||||
};
|
||||
|
||||
return httpPostInterceptor(url, data, headers);
|
||||
}
|
||||
Loading…
Reference in New Issue