fixing sabda
This commit is contained in:
parent
5fdcfdfdb9
commit
4f02c4ae18
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue