fixing sabda

This commit is contained in:
Sabda Yagra 2025-09-23 08:35:49 +07:00
parent 5fdcfdfdb9
commit 4f02c4ae18
8 changed files with 521 additions and 456 deletions

View File

@ -15,7 +15,7 @@ import { Badge } from "@/components/ui/badge";
import { format } from "date-fns";
import { useRouter } from "next/navigation";
import { deleteMedia } from "@/service/content/content";
import { error, loading } from "@/lib/swal";
import { error } from "@/lib/swal";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import Link from "next/link";
@ -41,7 +41,7 @@ const useTableColumns = () => {
{
accessorKey: "title",
header: "Title",
cell: ({ row }: { row: { getValue: (key: string) => string } }) => {
cell: ({ row }) => {
const title: string = row.getValue("title");
return (
<span className="whitespace-nowrap">
@ -56,24 +56,17 @@ const useTableColumns = () => {
cell: ({ row }) => {
const categoryName = row.getValue("categoryName");
const categories = row.original.categories;
// Handle new API structure with categories array
const displayName = categoryName || (categories && categories.length > 0 ? categories[0].title : "-");
return (
<span className="whitespace-nowrap">
{displayName}
</span>
);
const displayName =
categoryName ||
(categories && categories.length > 0 ? categories[0].title : "-");
return <span className="whitespace-nowrap">{displayName}</span>;
},
},
{
accessorKey: "createdAt",
header: "Upload Date",
cell: ({ row }) => {
const createdAt = row.getValue("createdAt") as
| string
| number
| undefined;
const createdAt = row.getValue("createdAt") as string | number | undefined;
const formattedDate =
createdAt && !isNaN(new Date(createdAt).getTime())
? format(new Date(createdAt), "dd-MM-yyyy HH:mm:ss")
@ -86,7 +79,7 @@ const useTableColumns = () => {
header: "Creator Group",
cell: ({ row }) => (
<span className="whitespace-nowrap">
{row.getValue("creatorName") || row.getValue("createdByName")}
{row.original.creatorName || row.original.createdByName || "-"}
</span>
),
},
@ -105,20 +98,19 @@ const useTableColumns = () => {
cell: ({ row }) => {
const isPublish = row.original.isPublish;
const isPublishOnPolda = row.original.isPublishOnPolda;
const creatorGroupParentLevelId =
row.original.creatorGroupParentLevelId;
const creatorGroupParentLevelId = row.original.creatorGroupParentLevelId;
let displayText = "-";
if (isPublish && !isPublishOnPolda) {
displayText = "Mabes";
} else if (isPublish && isPublishOnPolda) {
if (Number(creatorGroupParentLevelId) == 761) {
if (Number(creatorGroupParentLevelId) === 761) {
displayText = "Mabes & Satker";
} else {
displayText = "Mabes & Polda";
}
} else if (!isPublish && isPublishOnPolda) {
if (Number(creatorGroupParentLevelId) == 761) {
if (Number(creatorGroupParentLevelId) === 761) {
displayText = "Satker";
} else {
displayText = "Polda";
@ -132,7 +124,6 @@ const useTableColumns = () => {
);
},
},
//
{
accessorKey: "statusName",
header: "Status",
@ -140,18 +131,13 @@ const useTableColumns = () => {
const statusId = Number(row.original?.statusId);
const reviewedAtLevel = row.original?.reviewedAtLevel || "";
const creatorGroupLevelId = Number(row.original?.creatorGroupLevelId);
const needApprovalFromLevel = Number(
row.original?.needApprovalFromLevel
);
const needApprovalFromLevel = Number(row.original?.needApprovalFromLevel);
const userHasReviewed = reviewedAtLevel.includes(`:${userLevelId}:`);
const isCreator = creatorGroupLevelId === Number(userLevelId);
const isWaitingForReview =
statusId === 2 && !userHasReviewed && !isCreator;
const isApprovalNeeded =
statusId === 1 && needApprovalFromLevel === Number(userLevelId);
const isWaitingForReview = statusId === 2 && !userHasReviewed && !isCreator;
const isApprovalNeeded = statusId === 1 && needApprovalFromLevel === Number(userLevelId);
const label =
isWaitingForReview || isApprovalNeeded
@ -169,18 +155,12 @@ const useTableColumns = () => {
const statusStyles = colors[label] || colors.default;
return (
<Badge
className={cn(
"rounded-full px-5 w-full whitespace-nowrap",
statusStyles
)}
>
<Badge className={cn("rounded-full px-5 w-full whitespace-nowrap", statusStyles)}>
{label}
</Badge>
);
},
},
{
id: "actions",
accessorKey: "action",
@ -191,13 +171,8 @@ const useTableColumns = () => {
const MySwal = withReactContent(Swal);
async function doDelete(id: any) {
// loading();
const data = {
id,
};
const data = { id };
const response = await deleteMedia(data);
if (response?.error) {
error(response.message);
return false;
@ -221,7 +196,6 @@ const useTableColumns = () => {
const handleDeleteMedia = (id: any) => {
MySwal.fire({
title: "Hapus Data",
text: "",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#3085d6",
@ -241,9 +215,7 @@ const useTableColumns = () => {
React.useEffect(() => {
if (userLevelId !== undefined && roleId !== undefined) {
setIsMabesApprover(
Number(userLevelId) == 216 && Number(roleId) == 3
);
setIsMabesApprover(Number(userLevelId) === 216 && Number(roleId) === 3);
}
}, [userLevelId, roleId]);
@ -263,23 +235,14 @@ const useTableColumns = () => {
href={`/admin/content/image/detail/${row.original.id}`}
className="hover:text-black"
>
<DropdownMenuItem className="p-2 border-b text-default-700 group rounded-none">
<DropdownMenuItem className="p-2 border-b text-default-700 rounded-none">
<Eye className="w-4 h-4 me-1.5" />
View
</DropdownMenuItem>
</Link>
{/* <Link
href={`/contributor/content/image/update/${row.original.id}`}
>
<DropdownMenuItem className="p-2 border-b text-default-700 group rounded-none">
<SquarePen className="w-4 h-4 me-1.5" />
Edit
</DropdownMenuItem>
</Link> */}
{(Number(row.original.uploadedById) === Number(userId) ||
isMabesApprover) && (
{(Number(row.original.uploadedById) === Number(userId) || isMabesApprover) && (
<Link href={`/admin/content/image/update/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group rounded-none">
<DropdownMenuItem className="p-2 border-b text-default-700 rounded-none">
<SquarePen className="w-4 h-4 me-1.5" />
Edit
</DropdownMenuItem>
@ -287,20 +250,11 @@ const useTableColumns = () => {
)}
<DropdownMenuItem
onClick={() => handleDeleteMedia(row.original.id)}
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-white rounded-none"
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-white rounded-none"
>
<Trash2 className="w-4 h-4 me-1.5 focus:text-white" />
<Trash2 className="w-4 h-4 me-1.5" />
Delete
</DropdownMenuItem>
{/* {(row.original.uploadedById === userId || isMabesApprover) && (
<DropdownMenuItem
onClick={() => handleDeleteMedia(row.original.id)}
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-destructive-foreground rounded-none"
>
<Trash2 className="w-4 h-4 me-1.5" />
Hapus
</DropdownMenuItem>
)} */}
</DropdownMenuContent>
</DropdownMenu>
);

View File

@ -178,6 +178,51 @@ const TableImage = () => {
});
};
// async function fetchData() {
// const formattedStartDate = startDate
// ? format(new Date(startDate), "yyyy-MM-dd")
// : "";
// const formattedEndDate = endDate
// ? format(new Date(endDate), "yyyy-MM-dd")
// : "";
// try {
// // Using the new interface-based approach for image content
// const filters: ArticleFilters = {
// page: page,
// totalPage: Number(showData),
// title: search || undefined,
// categoryId: categoryFilter ? Number(categoryFilter) : undefined,
// typeId: 1, // image content type
// statusId: statusFilter?.length > 0 ? Number(statusFilter[0]) : undefined,
// startDate: formattedStartDate || undefined,
// endDate: formattedEndDate || undefined,
// };
// const res = await listArticlesWithFilters(filters);
// const data = res?.data?.data;
// // Handle new articles API response structure
// if (Array.isArray(data)) {
// data.forEach((item: any, index: number) => {
// item.no = (page - 1) * Number(showData) + index + 1;
// });
// setDataTable(data);
// setTotalData(data.length);
// setTotalPage(Math.ceil(data.length / Number(showData)));
// } else {
// // Fallback to old structure if API still returns old format
// const contentData = data?.content;
// contentData.forEach((item: any, index: number) => {
// item.no = (page - 1) * Number(showData) + index + 1;
// });
// setDataTable(contentData);
// setTotalData(data?.totalElements);
// setTotalPage(data?.totalPages);
// }
// } catch (error) {
// console.error("Error fetching tasks:", error);
// }
// }
async function fetchData() {
const formattedStartDate = startDate
? format(new Date(startDate), "yyyy-MM-dd")
@ -185,42 +230,47 @@ const TableImage = () => {
const formattedEndDate = endDate
? format(new Date(endDate), "yyyy-MM-dd")
: "";
try {
// Using the new interface-based approach for image content
const filters: ArticleFilters = {
page: page,
page,
totalPage: Number(showData),
title: search || undefined,
categoryId: categoryFilter ? Number(categoryFilter) : undefined,
typeId: 1, // image content type
statusId: statusFilter?.length > 0 ? Number(statusFilter[0]) : undefined,
statusId:
statusFilter?.length > 0 ? Number(statusFilter[0]) : undefined,
startDate: formattedStartDate || undefined,
endDate: formattedEndDate || undefined,
};
const res = await listArticlesWithFilters(filters);
const data = res?.data?.data;
// Handle new articles API response structure
if (Array.isArray(data)) {
data.forEach((item: any, index: number) => {
item.no = (page - 1) * Number(showData) + index + 1;
});
setDataTable(data);
// ✅ aman karena data array
const processed = data.map((item: any, index: number) => ({
...item,
no: (page - 1) * Number(showData) + index + 1,
}));
setDataTable(processed);
setTotalData(data.length);
setTotalPage(Math.ceil(data.length / Number(showData)));
} else {
// Fallback to old structure if API still returns old format
const contentData = data?.content;
contentData.forEach((item: any, index: number) => {
item.no = (page - 1) * Number(showData) + index + 1;
});
setDataTable(contentData);
setTotalData(data?.totalElements);
setTotalPage(data?.totalPages);
// ✅ fallback kalau masih pakai struktur lama
const contentData = Array.isArray(data?.content) ? data.content : [];
const processed = contentData.map((item: any, index: number) => ({
...item,
no: (page - 1) * Number(showData) + index + 1,
}));
setDataTable(processed);
setTotalData(data?.totalElements ?? 0);
setTotalPage(data?.totalPages ?? 1);
}
} catch (error) {
console.error("Error fetching tasks:", error);
} catch (err) {
console.error("Error fetching tasks:", err);
setDataTable([]); // fallback aman
}
}

View File

@ -197,7 +197,7 @@ export default function FormImageDetail() {
const getCategories = async () => {
try {
const category = await listEnableCategory(fileTypeId);
const resCategory: Category[] = category?.data?.data?.content;
const resCategory: Category[] = category?.data;
setCategories(resCategory);
console.log("data category", resCategory);
@ -211,7 +211,7 @@ export default function FormImageDetail() {
// setValue("categoryId", findCategory.id);
setSelectedCategory(findCategory.id); // Set the selected category
const response = await getTagsBySubCategoryId(findCategory.id);
setTags(response?.data?.data);
setTags(response?.data);
}
}
} catch (error) {
@ -227,40 +227,95 @@ export default function FormImageDetail() {
setFilePlacements(temp);
};
// useEffect(() => {
// async function initState() {
// if (id) {
// const response = await detailMedia(id);
// const details = response?.data;
// console.log("detail", details);
// setFiles(details?.files);
// setDetail(details);
// setMain({
// type: details?.fileType.name,
// url: details?.files[0]?.url,
// names: details?.files[0]?.fileName,
// format: details?.files[0]?.format,
// });
// setupPlacementCheck(details?.files?.length);
// if (details.publishedForObject) {
// const publisherIds = details.publishedForObject.map(
// (obj: any) => obj.id
// );
// setSelectedPublishers(publisherIds);
// }
// // Set the selected target to the category ID from details
// setSelectedTarget(String(details.category.id));
// const filesData = details.files || [];
// const fileUrls = filesData.map((file: { thumbnailFileUrl: string }) =>
// file.thumbnailFileUrl ? file.thumbnailFileUrl : "default-image.jpg"
// );
// setDetailThumb(fileUrls);
// const approvals = await getDataApprovalByMediaUpload(details?.id);
// setApproval(approvals?.data);
// }
// }
// initState();
// }, [refresh, setValue]);
useEffect(() => {
async function initState() {
if (id) {
const response = await detailMedia(id);
const details = response?.data?.data;
const details = response?.data;
console.log("detail", details);
setFiles(details?.files);
setDetail(details);
setMain({
type: details?.fileType.name,
url: details?.files[0]?.url,
names: details?.files[0]?.fileName,
format: details?.files[0]?.format,
});
setupPlacementCheck(details?.files?.length);
if (details.publishedForObject) {
// Set detail untuk ditampilkan
setDetail(details);
// Files
setFiles(details?.files);
// Ambil file pertama sebagai "main"
if (details?.files && details.files.length > 0) {
setMain({
type: "image", // atau mapping sendiri kalau ada typeId
url: details.files[0].file_url,
names: details.files[0].file_name,
format: details.files[0].file_name.split(".").pop(), // ambil ekstensi
});
}
setupPlacementCheck(details?.files?.length ?? 0);
// Kalau ada publishedForObject
if (details?.publishedForObject) {
const publisherIds = details.publishedForObject.map(
(obj: any) => obj.id
);
setSelectedPublishers(publisherIds);
}
// Set the selected target to the category ID from details
setSelectedTarget(String(details.category.id));
// Set target category
if (details?.categories && details.categories.length > 0) {
setSelectedTarget(String(details.categories[0].id));
} else if (details?.categoryId) {
setSelectedTarget(String(details.categoryId));
}
const filesData = details.files || [];
const fileUrls = filesData.map((file: { thumbnailFileUrl: string }) =>
file.thumbnailFileUrl ? file.thumbnailFileUrl : "default-image.jpg"
// Thumbnails
const filesData = details?.files || [];
const fileUrls = filesData.map((file: any) =>
file.file_thumbnail ? file.file_thumbnail : file.file_url
);
setDetailThumb(fileUrls);
// Ambil approval
const approvals = await getDataApprovalByMediaUpload(details?.id);
setApproval(approvals?.data?.data);
setApproval(approvals?.data);
}
}
initState();
@ -490,6 +545,7 @@ export default function FormImageDetail() {
<SelectContent>
{/* Show the category from details if it doesn't exist in categories list */}
{detail &&
Array.isArray(categories) &&
!categories.find(
(cat) =>
String(cat.id) === String(detail.category.id)
@ -501,12 +557,13 @@ export default function FormImageDetail() {
{detail.category.name}
</SelectItem>
)}
{categories.map((category) => (
{categories?.map((cat) => (
<SelectItem
key={String(category.id)}
value={String(category.id)}
key={String(cat.id)}
value={String(cat.id)}
>
{category.name}
{cat.name}
</SelectItem>
))}
</SelectContent>

View File

@ -474,24 +474,26 @@ export default function FormImage() {
// Use new Article Categories API
const category = await listArticleCategories(1, 100);
console.log("Article categories response:", category);
if (category?.error) {
console.error("Failed to fetch article categories:", category.message);
// Fallback to old API if new one fails
const fallbackCategory = await listEnableCategory(fileTypeId);
const resCategory: Category[] = fallbackCategory?.data.data.content || [];
const resCategory: Category[] =
fallbackCategory?.data.data.content || [];
setCategories(resCategory);
return;
}
// Handle new API response structure
const resCategory: Category[] = category?.data?.data?.map((item: any) => ({
id: item.id,
name: item.title, // map title to name for backward compatibility
title: item.title,
description: item.description,
...item
})) || [];
const resCategory: Category[] =
category?.data?.data?.map((item: any) => ({
id: item.id,
name: item.title, // map title to name for backward compatibility
title: item.title,
description: item.description,
...item,
})) || [];
setCategories(resCategory);
console.log("Article categories loaded:", resCategory);
@ -512,7 +514,8 @@ export default function FormImage() {
// Fallback to old API if error occurs
try {
const fallbackCategory = await listEnableCategory(fileTypeId);
const resCategory: Category[] = fallbackCategory?.data.data.content || [];
const resCategory: Category[] =
fallbackCategory?.data.data.content || [];
setCategories(resCategory);
} catch (fallbackError) {
console.error("Fallback category fetch also failed:", fallbackError);
@ -550,6 +553,8 @@ export default function FormImage() {
}
}, [articleBody, setValue]);
const userId = Cookies.get("userId"); // atau dari auth context / localStorage
const save = async (data: ImageSchema) => {
loading();
@ -560,7 +565,6 @@ export default function FormImage() {
const finalTags = tags.join(", ");
const finalTitle = isSwitchOn ? title : data.title;
// const finalDescription = articleBody || data.description;
const finalDescription = isSwitchOn
? data.description
: selectedFileType === "rewrite"
@ -572,125 +576,94 @@ export default function FormImage() {
return;
}
// New Articles API request data structure
function formatDateForBackend(date: Date) {
const pad = (n: number) => (n < 10 ? "0" + n : n);
return (
date.getFullYear() +
"-" +
pad(date.getMonth() + 1) +
"-" +
pad(date.getDate()) +
" " +
pad(date.getHours()) +
":" +
pad(date.getMinutes()) +
":" +
pad(date.getSeconds())
);
}
// ✅ Sesuaikan dengan struktur Swagger
const articleData: CreateArticleData = {
title: finalTitle,
aiArticleId: 0, // default 0
categoryIds: selectedCategory.toString(),
createdAt: formatDateForBackend(new Date()), // ✅ format sesuai backend
createdById: Number(userId), // isi dengan userId valid
description: htmlToString(finalDescription),
htmlDescription: finalDescription,
categoryIds: selectedCategory.toString(),
typeId: 1, // Image content type
tags: finalTags,
isDraft: true,
isPublish: false,
oldId: 0,
slug: finalTitle.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''),
};
// Keep old structure for backward compatibility if needed
let requestData: {
title: string;
description: string;
htmlDescription: string;
fileTypeId: string;
categoryId: any;
subCategoryId: any;
uploadedBy: string;
statusId: string;
publishedFor: string;
creatorName: string;
tags: string;
isYoutube: boolean;
isInternationalMedia: boolean;
attachFromScheduleId?: number;
} = {
...data,
title: finalTitle,
description: htmlToString(finalDescription),
htmlDescription: finalDescription,
fileTypeId,
categoryId: selectedCategory,
subCategoryId: selectedCategory,
uploadedBy: "2b7c8d83-d298-4b19-9f74-b07924506b58",
statusId: "1",
publishedFor: publishedFor.join(","),
creatorName: data.creatorName,
slug: finalTitle
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, ""),
tags: finalTags,
isYoutube: false,
isInternationalMedia: false,
title: finalTitle,
typeId: 1, // Image content type
};
let id = Cookies.get("idCreate");
if (scheduleId !== undefined) {
requestData.attachFromScheduleId = Number(scheduleId);
}
if (id == undefined) {
// Use new Articles API
const response = await createArticle(articleData);
console.log("Article Data Submitted:", articleData);
console.log("Article API Response:", response);
if (response?.error) {
MySwal.fire("Error", response.message || "Failed to create article", "error");
MySwal.fire(
"Error",
response.message || "Failed to create article",
"error"
);
return false;
}
// Get the article ID from the new API response
const articleId = response?.data?.data?.id;
Cookies.set("idCreate", articleId, { expires: 1 });
id = articleId;
// Upload files using new article-files API
// Upload files
const formData = new FormData();
// Add all files to FormData
files.forEach((file, index) => {
formData.append('files', file);
});
files.forEach((file) => formData.append("files", file));
console.log("Uploading files to article:", articleId);
console.log("Files to upload:", files.length);
try {
const uploadResponse = await uploadArticleFiles(articleId, formData);
if (uploadResponse?.error) {
MySwal.fire("Error", uploadResponse.message || "Failed to upload files", "error");
MySwal.fire(
"Error",
uploadResponse.message || "Failed to upload files",
"error"
);
return false;
}
console.log("Files uploaded successfully:", uploadResponse);
// Upload thumbnail using first file as thumbnail
// Upload thumbnail pakai file pertama
if (files.length > 0) {
const thumbnailFormData = new FormData();
thumbnailFormData.append('files', files[0]); // Use first file as thumbnail
console.log("Uploading thumbnail for article:", articleId);
try {
const thumbnailResponse = await uploadArticleThumbnail(articleId, thumbnailFormData);
if (thumbnailResponse?.error) {
console.warn("Thumbnail upload failed:", thumbnailResponse.message);
// Don't fail the whole process if thumbnail upload fails
} else {
console.log("Thumbnail uploaded successfully:", thumbnailResponse);
}
} catch (thumbnailError) {
console.warn("Thumbnail upload error:", thumbnailError);
// Don't fail the whole process if thumbnail upload fails
}
thumbnailFormData.append("files", files[0]);
await uploadArticleThumbnail(articleId, thumbnailFormData);
}
} catch (uploadError) {
console.error("Upload error:", uploadError);
MySwal.fire("Error", "Failed to upload files. Please try again.", "error");
MySwal.fire(
"Error",
"Failed to upload files. Please try again.",
"error"
);
return false;
}
// Show success message
MySwal.fire({
title: "Sukses",
text: "Article dan files berhasil disimpan.",
@ -700,28 +673,10 @@ export default function FormImage() {
}).then(() => {
router.push("/admin/content/image");
});
Cookies.remove("idCreate");
return;
}
// Keep old upload logic for backward compatibility
// const progressInfoArr = files.map((item) => ({
// percentage: 0,
// fileName: item.name,
// }));
// progressInfo = progressInfoArr;
// setIsStartUpload(true);
// setProgressList(progressInfoArr);
// files.map(async (item: any, index: number) => {
// await uploadResumableFile(
// index,
// String(id),
// item,
// fileTypeId == "2" || fileTypeId == "4" ? item.duration : "0"
// );
// });
Cookies.remove("idCreate");
};
@ -755,13 +710,12 @@ export default function FormImage() {
const resCsrf = await getCsrfToken();
const csrfToken = resCsrf?.data?.token;
console.log("CSRF TOKEN : ", csrfToken);
const headers = {
"X-XSRF-TOKEN": csrfToken,
};
const upload = new Upload(file, {
endpoint: `${process.env.NEXT_PUBLIC_API}/media/file/upload`,
endpoint: `${process.env.NEXT_PUBLIC_API}/articles/file/upload`,
headers: headers,
retryDelays: [0, 3000, 6000, 12_000, 24_000],
chunkSize: 20_000,

View File

@ -1,6 +1,12 @@
"use client";
import React, { Dispatch, SetStateAction, useState, useEffect } from "react";
import React, {
Dispatch,
SetStateAction,
useState,
useEffect,
ReactNode,
} from "react";
import Image from "next/image";
import { Icon } from "@iconify/react";
@ -15,8 +21,27 @@ interface RetractingSidebarProps {
sidebarData: boolean;
updateSidebarData: (newData: boolean) => void;
}
interface SidebarItemBase {
title: string;
icon: () => ReactNode;
}
const sidebarSections = [
interface SidebarLinkItem extends SidebarItemBase {
link: string;
}
interface SidebarParentItem extends SidebarItemBase {
children: SidebarLinkItem[];
}
type SidebarItem = SidebarLinkItem | SidebarParentItem;
interface SidebarSection {
title: string;
items: SidebarItem[];
}
const sidebarSections: SidebarSection[] = [
{
title: "Dashboard",
items: [
@ -33,29 +58,37 @@ const sidebarSections = [
title: "Content",
items: [
{
title: "Foto",
icon: () => <Icon icon="ri:article-line" className="text-lg" />,
link: "/admin/content/image",
},
{
title: "Audio Visual",
icon: () => <Icon icon="famicons:list-outline" className="text-lg" />,
link: "/admin/content/audio-visual",
},
{
title: "Teks",
icon: () => <Icon icon="ic:round-ads-click" className="text-lg" />,
link: "/admin/content/document",
},
{
title: "Audio",
icon: () => (
<Icon
icon="material-symbols:comment-outline-rounded"
className="text-lg"
/>
),
link: "/admin/content/audio",
title: "Master Data",
icon: () => <Icon icon="mdi:folder-outline" className="text-lg" />,
children: [
{
title: "Foto",
icon: () => <Icon icon="ri:article-line" className="text-lg" />,
link: "/admin/content/image",
},
{
title: "Audio Visual",
icon: () => (
<Icon icon="famicons:list-outline" className="text-lg" />
),
link: "/admin/content/audio-visual",
},
{
title: "Teks",
icon: () => <Icon icon="ic:round-ads-click" className="text-lg" />,
link: "/admin/content/document",
},
{
title: "Audio",
icon: () => (
<Icon
icon="material-symbols:comment-outline-rounded"
className="text-lg"
/>
),
link: "/admin/content/audio",
},
],
},
],
},
@ -174,6 +207,12 @@ const SidebarContent = ({
updateSidebarData: (newData: boolean) => void;
}) => {
const { theme, toggleTheme } = useTheme();
const [expanded, setExpanded] = useState<string | null>(null); // track parent yang dibuka
const toggleExpand = (title: string) => {
setExpanded((prev) => (prev === title ? null : title));
};
const handleLogout = () => {
Object.keys(Cookies.get()).forEach((cookieName) => {
Cookies.remove(cookieName);
@ -181,13 +220,13 @@ const SidebarContent = ({
window.location.href = "/";
};
return (
<div className="flex flex-col h-full">
{/* SCROLLABLE TOP SECTION */}
<div className="flex-1 overflow-y-auto">
{/* HEADER SECTION */}
<div className="flex flex-col space-y-6">
{/* Logo and Toggle */}
{/* Logo */}
<div className="flex items-center justify-between px-4 py-6">
<Link href="/" className="flex items-center space-x-3">
<div className="relative">
@ -230,35 +269,85 @@ const SidebarContent = ({
{/* Navigation Sections */}
<div className="space-y-3 px-3 pb-6">
{sidebarSections.map((section, sectionIndex) => (
{sidebarSections.map((section) => (
<motion.div
key={section.title}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 + sectionIndex * 0.1 }}
transition={{ delay: 0.1 }}
className="space-y-3"
>
{open && (
<motion.h3
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 + sectionIndex * 0.1 }}
transition={{ delay: 0.2 }}
className="text-xs font-semibold text-slate-500 uppercase tracking-wider px-3"
>
{section.title}
</motion.h3>
)}
<div className="space-y-2">
{section.items.map((item, itemIndex) => (
<Link href={item.link} key={item.title}>
<Option
Icon={item.icon}
title={item.title}
active={pathname === item.link}
open={open}
/>
</Link>
))}
{section.items.map((item) =>
"children" in item ? (
<div key={item.title}>
{/* Parent menu dengan toggle */}
<div
onClick={() => toggleExpand(item.title)}
className="w-full flex items-center justify-between pr-2"
>
<Option
Icon={item.icon}
title={item.title}
active={false}
open={open}
/>
{open && (
<motion.span
animate={{
rotate: expanded === item.title ? 90 : 0,
}}
transition={{ duration: 0.2 }}
className="text-slate-500"
>
<Icon icon="mdi:chevron-right" />
</motion.span>
)}
</div>
{/* Children expand/collapse */}
<motion.div
initial={false}
animate={{
height: expanded === item.title ? "auto" : 0,
opacity: expanded === item.title ? 1 : 0,
}}
className="overflow-hidden ml-6 space-y-1"
>
{item.children.map((child) => (
<Link href={child.link} key={child.title}>
<Option
Icon={child.icon}
title={child.title}
active={pathname === child.link}
open={open}
/>
</Link>
))}
</motion.div>
</div>
) : (
<Link href={item.link} key={item.title}>
<Option
Icon={item.icon}
title={item.title}
active={pathname === item.link}
open={open}
/>
</Link>
)
)}
</div>
</motion.div>
))}
@ -266,148 +355,20 @@ const SidebarContent = ({
</div>
</div>
{/* FIXED BOTTOM SECTION */}
<div className="flex-shrink-0 space-y-1 border-t border-slate-200/60 dark:border-slate-700/60 bg-white/50 dark:bg-slate-800/50 backdrop-blur-sm">
{/* Divider */}
{/* <div className="px-3 pb-2">
<div className="h-px bg-gradient-to-r from-transparent via-slate-300 to-transparent"></div>
</div> */}
{/* Theme Toggle */}
<div className="px-3 pt-1">
<motion.button
onClick={toggleTheme}
className={`relative flex h-12 w-full items-center rounded-xl transition-all duration-200 cursor-pointer group ${
open ? "px-3" : "justify-center"
} ${
theme === "dark"
? "bg-gradient-to-r from-emerald-500 to-green-500 text-white shadow-lg shadow-emerald-500/25"
: "text-slate-600 hover:bg-gradient-to-r hover:from-slate-100 hover:to-slate-200/50 hover:text-slate-800 dark:text-slate-300 dark:hover:bg-slate-700/50"
}`}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<motion.div
className={`h-full flex items-center justify-center ${
open ? "w-12" : "w-full"
}`}
>
<div
className={`text-lg transition-all duration-200 ${
theme === "dark"
? "text-white"
: "text-slate-500 group-hover:text-slate-700 dark:text-slate-400 dark:group-hover:text-slate-200"
}`}
>
{theme === "dark" ? (
<Icon icon="solar:sun-bold" className="text-lg" />
) : (
<Icon icon="solar:moon-bold" className="text-lg" />
)}
</div>
</motion.div>
{open && (
<motion.span
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1, duration: 0.2 }}
className={`text-sm font-medium transition-colors duration-200 ${
theme === "dark"
? "text-white"
: "text-slate-700 dark:text-slate-300"
}`}
>
{theme === "dark" ? "Light Mode" : "Dark Mode"}
</motion.span>
)}
</motion.button>
</div>
{/* Settings */}
<div className="px-3">
<Link href="/settings">
<Option
Icon={() => (
<Icon icon="lets-icons:setting-fill" className="text-lg" />
)}
title="Settings"
active={pathname === "/settings"}
open={open}
/>
</Link>
</div>
{/* User Profile */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="px-3 py-3 border-t border-slate-200/60"
>
<div
className={`${
open
? "flex items-center space-x-3"
: "flex items-center justify-center"
} p-3 rounded-xl bg-gradient-to-r from-slate-50 to-slate-100/50 hover:from-slate-100 hover:to-slate-200/50 transition-all duration-200 cursor-pointer group`}
>
<div className="relative">
<div className="w-10 h-10 rounded-full bg-gradient-to-r from-blue-500 to-purple-500 flex items-center justify-center text-white font-semibold text-sm shadow-lg">
A
</div>
<div className="absolute -bottom-1 -right-1 w-4 h-4 bg-green-500 rounded-full border-2 border-white"></div>
</div>
{open && (
<motion.div
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.5 }}
className="flex-1 min-w-0"
>
<p className="text-sm font-medium text-slate-800 truncate">
Mabes Polri - Approver
</p>
<Link href="/auth" onClick={handleLogout}>
<p className="text-xs text-slate-500 hover:text-blue-600 transition-colors duration-200">
Sign out
</p>
</Link>
</motion.div>
)}
</div>
</motion.div>
{/* Expand Button for Collapsed State */}
{/* {!open && (
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.6 }}
className="px-3 pt-2"
>
<button
onClick={() => updateSidebarData(true)}
className="w-full p-3 rounded-xl bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 text-white shadow-lg transition-all duration-200 hover:shadow-xl group"
>
<div className="flex items-center justify-center">
<Icon
icon="heroicons:chevron-right"
className="w-5 h-5 group-hover:scale-110 transition-transform duration-200"
/>
</div>
</button>
</motion.div>
)} */}
</div>
{/* ... (BOTTOM SECTION tetap sama) */}
</div>
);
};
const Sidebar = () => {
const [open, setOpen] = useState(true);
const [expanded, setExpanded] = useState<string | null>(null); // track submenu yg dibuka
const pathname = usePathname();
const toggleExpand = (title: string) => {
setExpanded((prev) => (prev === title ? null : title));
};
return (
<motion.nav
layout
@ -424,25 +385,12 @@ const Sidebar = () => {
className="w-5 h-5 text-zinc-400 border border-zinc-400 rounded-full flex justify-center items-center"
onClick={() => setOpen(true)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2.5"
d="m10 17l5-5m0 0l-5-5"
/>
</svg>
</button>
</div>
)}
{/* Logo + tombol collapse */}
<div
className={`flex ${
open ? "justify-between" : "justify-center"
@ -456,39 +404,81 @@ const Sidebar = () => {
className="w-5 h-5 text-zinc-400 border border-zinc-400 rounded-full flex justify-center items-center"
onClick={() => setOpen(false)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2.5"
d="m14 7l-5 5m0 0l5 5"
/>
</svg>
</button>
)}
</div>
<div className="space-y-1">
{/* Menu utama */}
<div className="space-y-3 mt-3">
{sidebarSections.map((section) => (
<div key={section.title}>
<p className="font-bold text-[14px] py-2">{section.title}</p>
{section.items.map((item) => (
<Link href={item.link} key={item.title}>
<Option
Icon={item.icon}
title={item.title}
active={pathname === item.link}
open={open}
/>
</Link>
))}
{open && (
<h3 className="px-2 text-xs font-semibold text-slate-500 uppercase tracking-wider">
{section.title}
</h3>
)}
{section.items.map((item) =>
"children" in item ? (
<div key={item.title}>
{/* Parent menu + chevron */}
<button
onClick={() => toggleExpand(item.title)}
className="w-full flex items-center justify-between pr-2"
>
<Option
Icon={item.icon}
title={item.title}
active={false}
open={open}
/>
{/* Chevron animasi */}
{open && (
<motion.span
animate={{
rotate: expanded === item.title ? 90 : 0,
}}
transition={{ duration: 0.2 }}
className="text-slate-500"
>
<Icon icon="mdi:chevron-right" />
</motion.span>
)}
</button>
{/* Children expand/collapse */}
<motion.div
initial={false}
animate={{
height: expanded === item.title ? "auto" : 0,
opacity: expanded === item.title ? 1 : 0,
}}
className="overflow-hidden ml-6 space-y-1"
>
{item.children.map((child) => (
<Link href={child.link} key={child.title}>
<Option
Icon={child.icon}
title={child.title}
active={pathname === child.link}
open={open}
/>
</Link>
))}
</motion.div>
</div>
) : (
<Link href={item.link} key={item.title}>
<Option
Icon={item.icon}
title={item.title}
active={pathname === item.link}
open={open}
/>
</Link>
)
)}
</div>
))}
</div>
@ -511,8 +501,8 @@ const Sidebar = () => {
active={pathname === "/settings"}
open={open}
/>
</Link>{" "}
<div className="flex flex-row gap-2">
</Link>
<div className="flex flex-row gap-2 px-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="34"
@ -526,7 +516,7 @@ const Sidebar = () => {
</svg>
<div className="flex flex-col gap-0.5 text-xs">
<p>admin-mabes</p>
<p className="underline">Logout</p>
<p className="underline cursor-pointer">Logout</p>
</div>
</div>
</div>
@ -535,7 +525,6 @@ const Sidebar = () => {
};
export default Sidebar;
const TitleSection = ({ open }: { open: boolean }) => {
return (
<div className="flex cursor-pointer items-center justify-between rounded-md transition-colors hover:bg-slate-100">

View File

@ -26,16 +26,19 @@ export interface ArticleFilters {
// Interface for creating new article
export interface CreateArticleData {
title: string;
aiArticleId: number;
categoryIds: string;
createdAt: string;
createdById: number;
description: string;
htmlDescription: string;
categoryIds: string;
typeId: number;
tags: string;
isDraft: boolean;
isPublish: boolean;
oldId: number;
slug: string;
tags: string;
title: string;
typeId: number;
}
// Interface for Article Category
@ -240,7 +243,10 @@ export async function uploadThumbnail(id: any, data: any) {
}
// New Articles API - Upload Article Files
export async function uploadArticleFiles(articleId: string | number, files: FormData) {
export async function uploadArticleFiles(
articleId: string | number,
files: FormData
) {
const url = `article-files/${articleId}`;
const headers = {
"Content-Type": "multipart/form-data",
@ -249,7 +255,10 @@ export async function uploadArticleFiles(articleId: string | number, files: Form
}
// New Articles API - Upload Article Thumbnail
export async function uploadArticleThumbnail(articleId: string | number, thumbnail: FormData) {
export async function uploadArticleThumbnail(
articleId: string | number,
thumbnail: FormData
) {
const url = `articles/thumbnail/${articleId}`;
const headers = {
"Content-Type": "multipart/form-data",
@ -258,7 +267,10 @@ export async function uploadArticleThumbnail(articleId: string | number, thumbna
}
// New Articles API - Get Article Categories
export async function listArticleCategories(page: number = 1, limit: number = 100) {
export async function listArticleCategories(
page: number = 1,
limit: number = 100
) {
const url = `article-categories?page=${page}&limit=${limit}`;
return httpGetInterceptor(url);
}
@ -336,7 +348,7 @@ export async function listArticles(
endDate?: string
) {
let url = `articles?page=${page}&totalPage=${totalPage}`;
// Add optional query parameters based on available filters
if (title) url += `&title=${encodeURIComponent(title)}`;
if (description) url += `&description=${encodeURIComponent(description)}`;
@ -351,7 +363,7 @@ export async function listArticles(
if (isDraft !== undefined) url += `&isDraft=${isDraft}`;
if (startDate) url += `&startDate=${startDate}`;
if (endDate) url += `&endDate=${endDate}`;
return await httpGetInterceptor(url);
}
@ -394,8 +406,9 @@ export async function listDataTeksNew(
) {
// Convert old parameters to new API format
const categoryId = categoryFilter ? Number(categoryFilter) : undefined;
const statusId = statusFilter?.length > 0 ? Number(statusFilter[0]) : undefined;
const statusId =
statusFilter?.length > 0 ? Number(statusFilter[0]) : undefined;
return await listArticles(
page + 1, // API expects 1-based page
Number(size),
@ -413,4 +426,4 @@ export async function listDataTeksNew(
startDate,
endDate
);
}
}

View File

@ -5,7 +5,7 @@ import {
} from "../http-config/http-interceptor-service";
export async function detailMedia(id: any) {
const url = `media?id=${id}`;
const url = `articles?id=${id}`;
return httpGetInterceptor(url);
}

View File

@ -59,6 +59,46 @@ export async function httpGetInterceptor(pathUrl: any) {
}
}
// export async function httpPostInterceptor(
// pathUrl: any,
// data?: any,
// headers?: any
// ) {
// const resCsrf = await getCsrfToken();
// const csrfToken = resCsrf?.data?.token;
// const defaultHeaders = {
// "Content-Type": "application/json",
// };
// const mergedHeaders = {
// ...defaultHeaders,
// ...(csrfToken ? { "X-XSRF-TOKEN": csrfToken } : {}),
// ...headers,
// };
// const response = await axiosInterceptorInstance
// .post(pathUrl, data, { headers: mergedHeaders })
// .catch((error) => error.response);
// console.log("Response interceptor : ", response);
// if (response?.status == 200 || response?.status == 201) {
// return {
// error: false,
// message: "success",
// data: response?.data,
// };
// } else if (response?.status == 401) {
// Object.keys(Cookies.get()).forEach((cookieName) => {
// Cookies.remove(cookieName);
// });
// window.location.href = "/";
// } else {
// return {
// error: true,
// message: response?.data?.message || response?.data || null,
// data: null,
// };
// }
// }
export async function httpPostInterceptor(
pathUrl: any,
data?: any,
@ -67,19 +107,27 @@ export async function httpPostInterceptor(
const resCsrf = await getCsrfToken();
const csrfToken = resCsrf?.data?.token;
const token = Cookies.get("token"); // JWT / session token
const clientKey = process.env.NEXT_PUBLIC_CLIENT_KEY; // dari .env.local
const defaultHeaders = {
"Content-Type": "application/json",
...(csrfToken ? { "X-XSRF-TOKEN": csrfToken } : {}),
...(token ? { Authorization: `Bearer ${token}` } : {}),
...(clientKey ? { "X-Client-Key": clientKey } : {}),
};
const mergedHeaders = {
...defaultHeaders,
...(csrfToken ? { "X-XSRF-TOKEN": csrfToken } : {}),
...headers,
};
const response = await axiosInterceptorInstance
.post(pathUrl, data, { headers: mergedHeaders })
.catch((error) => error.response);
console.log("Response interceptor : ", response);
if (response?.status == 200 || response?.status == 201) {
return {
error: false,