fix: detail article in admin page

This commit is contained in:
Sabda Yagra 2025-10-14 15:17:22 +07:00
parent 2e86d97aee
commit 0deb587a82
13 changed files with 2171 additions and 1812 deletions

View File

@ -77,12 +77,21 @@ const useTableColumns = () => {
return <span className="whitespace-nowrap">{formattedDate}</span>;
},
},
// {
// accessorKey: "creatorName",
// header: "Creator Group",
// cell: ({ row }) => (
// <span className="whitespace-nowrap">
// {row.getValue("creatorName") || row.getValue("createdByName")}
// </span>
// ),
// },
{
accessorKey: "creatorName",
header: "Creator Group",
cell: ({ row }) => (
<span className="whitespace-nowrap">
{row.getValue("creatorName") || row.getValue("createdByName")}
{row.original.creatorName || row.original.createdByName || "-"}
</span>
),
},

View File

@ -1,9 +1,11 @@
import FormAudioDetail from "@/components/form/content/audio/audio-detail-form";
import SiteBreadcrumb from "@/components/site-breadcrumb";
const AudioDetailPage = async () => {
return (
<div>
<div className="space-y-4">
<SiteBreadcrumb />
<div className="space-y-4 bg-slate-100">
<FormAudioDetail />
</div>
</div>

View File

@ -77,12 +77,21 @@ const useTableColumns = () => {
return <span className="whitespace-nowrap">{formattedDate}</span>;
},
},
// {
// accessorKey: "creatorName",
// header: "Creator Group",
// cell: ({ row }) => (
// <span className="whitespace-nowrap">
// {row.getValue("creatorName") || row.getValue("createdByName")}
// </span>
// ),
// },
{
accessorKey: "creatorName",
header: "Creator Group",
cell: ({ row }) => (
<span className="whitespace-nowrap">
{row.getValue("creatorName") || row.getValue("createdByName")}
{row.original.creatorName || row.original.createdByName || "-"}
</span>
),
},

View File

@ -66,7 +66,6 @@ import useTableColumns from "./columns";
const TableTeks = () => {
const router = useRouter();
const searchParams = useSearchParams();
const [dataTable, setDataTable] = React.useState<any[]>([]);
const [totalData, setTotalData] = React.useState<number>(1);
const [sorting, setSorting] = React.useState<SortingState>([]);
@ -76,18 +75,14 @@ const TableTeks = () => {
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [showData, setShowData] = React.useState("50");
const [showData, setShowData] = React.useState("10");
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: Number(showData),
});
const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1);
const [limit, setLimit] = React.useState(10);
const [search, setSearch] = React.useState<string>("");
const userId = getCookiesDecrypt("uie");
const userLevelId = getCookiesDecrypt("ulie");
const [categories, setCategories] = React.useState<any[]>([]);
const [selectedCategories, setSelectedCategories] = React.useState<number[]>(
[]
@ -99,7 +94,6 @@ const TableTeks = () => {
const [filterByCreator, setFilterByCreator] = React.useState("");
const [filterBySource, setFilterBySource] = React.useState("");
const [filterByCreatorGroup, setFilterByCreatorGroup] = React.useState("");
const roleId = getCookiesDecrypt("urie");
const columns = useTableColumns();
const table = useReactTable({
@ -179,13 +173,12 @@ const TableTeks = () => {
: "";
try {
// Using the new interface-based approach for video content
const filters: ArticleFilters = {
page: page,
page,
totalPage: Number(showData),
title: search || undefined,
categoryId: categoryFilter ? Number(categoryFilter) : undefined,
typeId: 3,
typeId: 3, // 🔹 text content
statusId:
statusFilter?.length > 0 ? Number(statusFilter[0]) : undefined,
startDate: formattedStartDate || undefined,
@ -194,14 +187,17 @@ const TableTeks = () => {
const res = await listArticlesWithFilters(filters);
const data = res?.data?.data;
const meta = res?.data?.meta;
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)));
setTotalData(meta?.count ?? data.length);
setTotalPage(
meta?.totalPage ?? Math.ceil(data.length / Number(showData))
);
} else if (Array.isArray(data?.content)) {
const contentData = data.content;
contentData.forEach((item: any, index: number) => {
@ -218,7 +214,7 @@ const TableTeks = () => {
setTotalPage(1);
}
} catch (error) {
console.error("Error fetching tasks:", error);
console.error("Error fetching text content:", error);
setDataTable([]);
setTotalData(0);
setTotalPage(1);

View File

@ -1,10 +1,11 @@
import FormTeks from "@/components/form/content/document/teks-form";
import SiteBreadcrumb from "@/components/site-breadcrumb";
const TeksCreatePage = async () => {
return (
<div>
{/* <SiteBreadcrumb /> */}
<div className="space-y-4">
<SiteBreadcrumb />
<div className="space-y-4 bg-slate-100">
<FormTeks />
</div>
</div>

View File

@ -1,10 +1,11 @@
import FormTeksDetail from "@/components/form/content/document/teks-detail-form";
import SiteBreadcrumb from "@/components/site-breadcrumb";
const TeksDetailPage = async () => {
return (
<div>
{/* <SiteBreadcrumb /> */}
<div className="space-y-4">
<SiteBreadcrumb />
<div className="space-y-4 bg-slate-100">
<FormTeksDetail />
</div>
</div>

View File

@ -21,8 +21,6 @@ import { Checkbox } from "@/components/ui/checkbox";
import Cookies from "js-cookie";
import {
getArticleDetail,
getTagsBySubCategoryId,
listEnableCategory,
publishMedia,
submitApproval,
} from "@/service/content/content";
@ -47,7 +45,6 @@ import { getCookiesDecrypt } from "@/lib/utils";
import { Icon } from "@iconify/react/dist/iconify.js";
import { error } from "@/lib/swal";
import dynamic from "next/dynamic";
import SuggestionModal from "@/components/modal/suggestions-modal";
import { formatDateToIndonesian } from "@/utils/globals";
import ApprovalHistoryModal from "@/components/modal/approval-history-modal";
import { listArticleCategories } from "@/service/content";
@ -62,19 +59,13 @@ const ViewEditor = dynamic(() => import("@/components/editor/view-editor"), {
ssr: false,
});
type Category = {
id: string;
title: string;
};
type Category = { id: string; title: string };
type FileType = {
id: number;
fileName: string;
fileUrl?: string;
fileThumbnail?: string;
format?: string;
};
type Detail = {
id: number;
title: string;
@ -137,46 +128,28 @@ export default function FormVideoDetail() {
const MySwal = withReactContent(Swal);
const router = useRouter();
const { id } = useParams() as { id: string };
const userId = getCookiesDecrypt("uie");
const userLevelId = getCookiesDecrypt("ulie");
const userLevelName = Cookies.get("state");
const roleId = getCookiesDecrypt("urie");
const [detail, setDetail] = useState<Detail | null>(null);
const [categories, setCategories] = useState<Category[]>([]);
const [files, setFiles] = useState<FileType[]>([]);
const [thumbsSwiper, setThumbsSwiper] = useState<any>(null);
const [approval, setApproval] = useState<any>();
const [selectedPublishers, setSelectedPublishers] = useState<number[]>([]);
const [isUserMabesApprover, setIsUserMabesApprover] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [status, setStatus] = useState("");
const [description, setDescription] = useState("");
const [filePlacements, setFilePlacements] = useState<string[][]>([]);
const [isUserMabesApprover, setIsUserMabesApprover] = useState(false);
const [refresh, setRefresh] = useState(false);
const [detailVideos, setDetailVideos] = useState<string[]>([]);
const [selectedCategory, setSelectedCategory] = useState<string>("");
const {
control,
formState: { errors },
} = useForm({
resolver: zodResolver(videoSchema),
});
const getStatusName = (statusId: number): string => {
const statusMap: { [key: number]: string } = {
1: "Menunggu Review",
2: "Diterima",
3: "Minta Update",
4: "Ditolak",
};
return statusMap[statusId] || "Unknown";
};
const { control } = useForm({ resolver: zodResolver(videoSchema) });
useEffect(() => {
if (userLevelId == "216" && roleId == "3") {
setIsUserMabesApprover(true);
}
if (userLevelId == "216" && roleId == "3") setIsUserMabesApprover(true);
}, [userLevelId, roleId]);
useEffect(() => {
@ -193,29 +166,16 @@ export default function FormVideoDetail() {
try {
const response = await getArticleDetail(Number(id));
const details = response?.data?.data;
setSelectedCategory(String(details.categories[0].id));
const mappedDetail: Detail = {
...details,
statusId: details?.statusId,
categoryId: details?.categories?.[0]?.id,
htmlDescription: details?.htmlDescription,
statusName: getStatusName(details?.statusId),
uploadedById: details?.createdById,
files: details?.files || [],
categories: details?.categories || [],
};
const mappedFiles =
details?.files?.map((f: any) => ({
id: f.id,
fileName: f.fileName,
fileUrl: f.fileUrl,
fileThumbnail: f.fileThumbnail,
format: f.format || "mp4",
})) || [];
setDetail(mappedDetail);
setFiles(mappedFiles);
setDetailVideos(mappedFiles.map((f: any) => f.fileUrl || ""));
setFiles(details?.files || []);
const approvals = await getDataApprovalByMediaUpload(mappedDetail.id);
setApproval(approvals?.data?.data);
} catch (err) {
@ -223,33 +183,32 @@ export default function FormVideoDetail() {
}
}
fetchDetail();
}, [id, refresh]);
}, [id]);
const getStatusName = (statusId: number) => {
const map: Record<number, string> = {
1: "Menunggu Review",
2: "Diterima",
3: "Minta Update",
4: "Ditolak",
};
return map[statusId] || "Unknown";
};
const handleCheckboxChange = (id: number) => {
setSelectedPublishers((prev) =>
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]
prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id]
);
};
const actionApproval = (e: string) => {
const temp = [];
for (const element of files) temp.push([]);
setFilePlacements(temp);
setStatus(e);
const actionApproval = (type: string) => {
const placements = files.map(() => []);
setFilePlacements(placements);
setStatus(type);
setDescription("");
setModalOpen(true);
};
const submit = async () => {
if (
(description?.length > 1 && Number(status) == 3) ||
Number(status) == 2 ||
Number(status) == 4
) {
await save();
}
};
const save = async () => {
const data = {
action: status == "2" ? "approve" : status == "3" ? "revision" : "reject",
@ -257,8 +216,8 @@ export default function FormVideoDetail() {
};
setModalOpen(false);
loading();
const response = await submitApproval(id, data);
if (response?.error) return error(response.message);
const res = await submitApproval(id, data);
if (res?.error) return error(res.message);
close();
MySwal.fire({
title: "Sukses",
@ -272,23 +231,15 @@ export default function FormVideoDetail() {
successCallback();
};
if (!detail) {
return (
<div className="text-center py-20 text-gray-500">
Memuat detail video...
</div>
);
}
if (!detail) return <div className="p-10 text-gray-500">Memuat data...</div>;
return (
<form>
<div className="flex flex-col lg:flex-row gap-10 border rounded-lg">
{/* LEFT SIDE */}
<Card className="w-full lg:w-8/12 m-2">
<div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">Form Video</p>
{/* Title */}
<div className="space-y-2 py-3">
<Label>Title</Label>
<Controller
@ -296,31 +247,22 @@ export default function FormVideoDetail() {
name="title"
render={({ field }) => (
<Input
type="text"
value={detail?.title}
value={detail.title}
onChange={field.onChange}
placeholder="Enter Title"
disabled
/>
)}
/>
{/* {errors.title?.message && (
<p className="text-red-400 text-sm">{errors.title.message}</p>
)} */}
</div>
{/* Category */}
<div className="py-3 w-full space-y-2">
<Label>Category</Label>
<Select
value={selectedCategory}
onValueChange={setSelectedCategory}
disabled
>
<Select disabled value={String(detail.categoryId)}>
<SelectTrigger>
<SelectValue placeholder="Pilih kategori" />
</SelectTrigger>
<SelectContent>
{categories?.map((cat) => (
{categories.map((cat) => (
<SelectItem key={cat.id} value={String(cat.id)}>
{cat.title}
</SelectItem>
@ -328,29 +270,12 @@ export default function FormVideoDetail() {
</SelectContent>
</Select>
</div>
{/* <div className="py-3 space-y-2">
<Label>Category</Label>
<Select disabled value={String(detail.categoryId)}>
<SelectTrigger>
<SelectValue placeholder="Pilih" />
</SelectTrigger>
<SelectContent>
{categories.map((cat) => (
<SelectItem key={cat.id} value={String(cat.id)}>
{cat.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div> */}
{/* Description */}
<div className="py-3 space-y-2">
<Label>Description</Label>
<ViewEditor initialData={detail.htmlDescription} />
</div>
{/* Video Files */}
<div className="space-y-2">
<Label className="text-xl">File Media</Label>
<Swiper
@ -391,24 +316,19 @@ export default function FormVideoDetail() {
</div>
</Card>
{/* RIGHT SIDE */}
<div className="w-full lg:w-4/12 m-2">
<Card className="pb-3">
<div className="px-3 py-3">
<Label>Creator</Label>
<Input
value={detail.createdByName}
disabled
placeholder="Creator"
/>
<Input value={detail.createdByName} disabled />
</div>
<div className="mt-3 px-3 space-y-2">
<Label>Preview</Label>
<Card className="mt-2 w-fit">
<img
src={detail.thumbnailUrl}
alt="Thumbnail"
src={detail.thumbnailUrl || detail.thumbnailLink}
alt="Thumbnail Gambar Utama"
className="h-[200px] rounded"
/>
</Card>
@ -417,10 +337,10 @@ export default function FormVideoDetail() {
<div className="px-3 py-3">
<Label>Tags</Label>
<div className="flex flex-wrap gap-2">
{detail.tags?.split(",").map((tag: string, index: number) => (
{detail.tags?.split(",").map((tag, i) => (
<Badge
key={index}
className="border rounded-md bg-black text-white px-2 py-2"
key={i}
className="border bg-black text-white px-2 py-2"
>
{tag.trim()}
</Badge>
@ -436,24 +356,18 @@ export default function FormVideoDetail() {
id={String(target)}
checked={selectedPublishers.includes(target)}
onChange={() => handleCheckboxChange(target)}
className="border"
/>
<Label htmlFor={String(target)}>
{target === 5 ? "UMUM" : target === 6 ? "JOURNALIS" : ""}
{target === 5 ? "UMUM" : "JOURNALIS"}
</Label>
</div>
))}
</div>
{/* <SuggestionModal
id={Number(id)}
numberOfSuggestion={detail?.numberOfSuggestion}
/> */}
<div className="px-3 py-3 border mx-3">
<p>Information:</p>
<p className="text-sm text-slate-400">
{detail.statusName || getStatusName(detail.statusId)}
{detail.statusId && getStatusName(detail.statusId)}
</p>
<p>Komentar</p>
<p>{approval?.message}</p>
@ -471,6 +385,108 @@ export default function FormVideoDetail() {
</Button>
</div>
)}
{(Number(detail.needApprovalFromLevel || 0) ==
Number(userLevelId) ||
(detail.isPublish === false && detail.statusId == 1)) &&
Number(detail.uploadedById || detail.createdById) !=
Number(userId) ? (
<div className="flex flex-col gap-2 p-3">
<Button
onClick={() => actionApproval("2")}
color="primary"
type="button"
>
<Icon icon="fa:check" className="mr-3" /> Accept
</Button>
<Button
onClick={() => actionApproval("3")}
className="bg-orange-400 hover:bg-orange-300"
type="button"
>
<Icon icon="fa:comment-o" className="mr-3" /> Revision
</Button>
<Button
onClick={() => actionApproval("4")}
color="destructive"
type="button"
>
<Icon icon="fa:times" className="mr-3" /> Reject
</Button>
</div>
) : null}
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
<DialogContent className="max-h-[600px] overflow-y-auto">
<DialogHeader>
<DialogTitle>Leave Comment</DialogTitle>
</DialogHeader>
{status === "2" &&
files.map((file, i) => (
<div key={i} className="flex items-center gap-4 mb-3">
<video
src={file.fileUrl}
className="w-[200px] h-[120px]"
controls
/>
<span>{file.fileName}</span>
</div>
))}
<Textarea
placeholder="Tulis komentar Anda..."
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<div className="flex flex-wrap gap-2 mt-2">
{status === "3" || status === "4" ? (
<>
{[
"Kualitas media kurang baik",
"Deskripsi kurang lengkap",
"Judul kurang tepat",
].map((text) => (
<Button
key={text}
size="sm"
variant={description === text ? "default" : "outline"}
onClick={() => setDescription(text)}
>
{text}
</Button>
))}
</>
) : (
<>
{["Konten sangat bagus", "Konten menarik"].map((text) => (
<Button
key={text}
size="sm"
variant={description === text ? "default" : "outline"}
onClick={() => setDescription(text)}
>
{text}
</Button>
))}
</>
)}
</div>
<DialogFooter className="mt-4 flex justify-end gap-3">
<Button onClick={() => save()} color="primary">
Submit
</Button>
<Button
color="destructive"
onClick={() => setModalOpen(false)}
>
Cancel
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Card>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@ -256,8 +256,11 @@ export default function FormAudioDetail() {
setDetail(details);
setMain({
type: details?.fileType.name,
url: details?.files[0]?.url,
type:
details?.fileType?.name ||
details?.fileTypeName || // kalau backend pakai nama ini
details?.typeName || // atau nama alternatif
"Audio", // fallback aman url: details?.files[0]?.url,
names: details?.files[0]?.fileName,
format: details?.files[0]?.format,
});
@ -448,8 +451,8 @@ export default function FormAudioDetail() {
return (
<form>
{detail !== undefined ? (
<div className="flex flex-col lg:flex-row gap-10">
<Card className="w-full lg:w-8/12">
<div className="flex flex-col lg:flex-row gap-10 border rounded-lg">
<Card className="w-full lg:w-8/12 m-2">
<div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">Form Audio</p>
<div className="gap-5 mb-5">
@ -533,31 +536,12 @@ export default function FormAudioDetail() {
</div>
</div>
</Card>
<div className="w-full lg:w-4/12">
<div className="w-full lg:w-4/12 m-2">
<Card className="pb-3">
<div className="px-3 py-3">
<div className="space-y-2">
<Label>Creator</Label>
<Controller
control={control}
name="creatorName"
render={({ field }) => (
<Input
type="text"
value={detail?.creatorName}
onChange={field.onChange}
placeholder="Enter Title"
/>
)}
/>
{errors.creatorName?.message && (
<p className="text-red-400 text-sm">
{errors.creatorName.message}
</p>
)}
</div>
<Label>Creator</Label>
<Input value={detail.createdByName} disabled />
</div>
<div className="px-3 py-3">
<div className="space-y-2">
<Label>Tags</Label>
@ -567,7 +551,7 @@ export default function FormAudioDetail() {
.map((tag: string, index: number) => (
<Badge
key={index}
className="border rounded-md px-2 py-2"
className="border rounded-md bg-black text-white px-2 py-2"
>
{tag.trim()}
</Badge>
@ -576,40 +560,26 @@ export default function FormAudioDetail() {
</div>
</div>
<div className="px-3 py-3">
<div className="flex flex-col gap-6 space-y-2">
<div className="flex flex-col gap-2 space-y-2">
<Label>Publish Target</Label>
<div className="flex gap-2 items-center">
<Checkbox
id="5"
id="4"
checked={selectedPublishers.includes(5)}
onChange={() => handleCheckboxChange(5)}
className="border"
/>
<Label htmlFor="5">UMUM</Label>
</div>
<div className="flex gap-2 items-center">
<Checkbox
id="6"
id="5"
checked={selectedPublishers.includes(6)}
onChange={() => handleCheckboxChange(6)}
className="border"
/>
<Label htmlFor="6">JOURNALIS</Label>
</div>
<div className="flex gap-2 items-center">
<Checkbox
id="7"
checked={selectedPublishers.includes(7)}
onChange={() => handleCheckboxChange(7)}
/>
<Label htmlFor="7">POLRI</Label>
</div>
<div className="flex gap-2 items-center">
<Checkbox
id="8"
checked={selectedPublishers.includes(8)}
onChange={() => handleCheckboxChange(8)}
/>
<Label htmlFor="8">KSP</Label>
</div>
</div>
</div>
<SuggestionModal

View File

@ -66,6 +66,11 @@ import FileTextPreview from "../file-preview-text";
import FileTextThumbnail from "../file-text-thumbnail";
import { listArticleCategories } from "@/service/content";
type Option = {
id: string;
label: string;
};
const imageSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }),
description: z
@ -120,13 +125,11 @@ export default function FormTeksDetail() {
const userId = getCookiesDecrypt("uie");
const userLevelId = getCookiesDecrypt("ulie");
const roleId = getCookiesDecrypt("urie");
const [modalOpen, setModalOpen] = useState(false);
const { id } = useParams() as { id: string };
console.log(id);
const editor = useRef(null);
type ImageSchema = z.infer<typeof imageSchema>;
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const taskId = Cookies.get("taskId");
const scheduleId = Cookies.get("scheduleId");
@ -146,11 +149,16 @@ export default function FormTeksDetail() {
const [files, setFiles] = useState<FileType[]>([]);
const [rejectedFiles, setRejectedFiles] = useState<number[]>([]);
const [isMabesApprover, setIsMabesApprover] = useState(false);
const [filePlacements, setFilePlacements] = useState<string[][]>([]);
const [isUserMabesApprover, setIsUserMabesApprover] = useState(false);
const [approval, setApproval] = useState<any>();
const [publishedFor, setPublishedFor] = useState<string[]>([]);
const options: Option[] = [
{ id: "all", label: "SEMUA" },
{ id: "4", label: "UMUM" },
{ id: "5", label: "JOURNALIS" },
];
let fileTypeId = "3";
@ -218,43 +226,63 @@ export default function FormTeksDetail() {
useEffect(() => {
async function initState() {
if (id) {
if (!id) return;
try {
const response = await getArticleDetail(Number(id));
const details = response?.data?.data;
console.log("detail", details);
setSelectedCategory(String(details.categories[0].id));
if (!details) return;
setFiles(details?.files);
// ✅ Aman untuk categories
const firstCategoryId =
details?.categories && details.categories.length > 0
? String(details.categories[0].id)
: "";
setSelectedCategory(firstCategoryId);
setFiles(details?.files || []);
setDetail(details);
// ✅ Aman untuk fileType
setMain({
type: details?.fileType.name,
url: details?.files[0]?.url,
names: details?.files[0]?.fileName,
format: details?.files[0]?.format,
type: details?.fileType?.name || "Unknown",
url: details?.files?.[0]?.url || "",
names: details?.files?.[0]?.fileName || "",
format: details?.files?.[0]?.format || "",
});
if (details.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));
setSelectedTarget(String(details?.category?.id || ""));
const filesData = details?.files || [];
const fileUrls = filesData.map((file: any) => {
const ext = file?.fileName?.includes(".")
? "." + file.fileName.split(".").pop()
: file?.type || "";
return {
url: file?.url || file?.secondaryUrl || "default-image.jpg",
format: ext?.toLowerCase(),
fileName: file?.fileName || "Unknown file",
type: file?.type || "",
};
});
const filesData = details.files || [];
const fileUrls = filesData.map((file: any) => ({
url: file.secondaryUrl || "default-image.jpg",
format: file.format,
fileName: file.fileName,
}));
setDetailThumb(fileUrls);
const approvals = await getDataApprovalByMediaUpload(details?.id);
setApproval(approvals?.data?.data);
} catch (err) {
console.error("Error loading article details:", err);
}
}
initState();
}, [refresh, setValue]);
@ -392,15 +420,15 @@ export default function FormTeksDetail() {
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then(() => {
router.push("/admin/content/document");
router.push("/admin/content/text");
});
};
return (
<form>
{detail !== undefined ? (
<div className="flex flex-col lg:flex-row gap-10">
<Card className="w-full lg:w-8/12">
<div className="flex flex-col lg:flex-row gap-10 border rounded-lg">
<Card className="w-full lg:w-8/12 m-2">
<div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">Form Text</p>
<div className="gap-5 mb-5">
@ -463,7 +491,7 @@ export default function FormTeksDetail() {
)}
</div>
<div className="space-y-2">
<Label className="text-xl">File Media </Label>
<Label className="text-xl">File Media</Label>
<div className="w-full">
<Swiper
thumbs={{ swiper: thumbsSwiper }}
@ -498,30 +526,12 @@ export default function FormTeksDetail() {
</div>
</div>
</Card>
<div className="w-full lg:w-4/12">
<div className="w-full lg:w-4/12 m-2">
<Card className="pb-3">
<div className="px-3 py-3">
<div className="space-y-2">
<div className="px-3 py-3">
<Label>Creator</Label>
<Controller
control={control}
name="creatorName"
render={({ field }) => (
<Input
type="text"
value={detail?.creatorName}
onChange={field.onChange}
placeholder="Enter Title"
/>
)}
/>
{errors.creatorName?.message && (
<p className="text-red-400 text-sm">
{errors.creatorName.message}
</p>
)}
<Input value={detail.createdByName} disabled />
</div>
</div>
{/* <div className="mt-3 px-3">
<Label>Pratinjau Gambar Utama</Label>
<Card className="mt-2">
@ -541,7 +551,7 @@ export default function FormTeksDetail() {
.map((tag: string, index: number) => (
<Badge
key={index}
className="border rounded-md px-2 py-2"
className="border rounded-md bg-black text-white px-2 py-2"
>
{tag.trim()}
</Badge>
@ -550,40 +560,26 @@ export default function FormTeksDetail() {
</div>
</div>
<div className="px-3 py-3">
<div className="flex flex-col gap-6 space-y-2">
<div className="flex flex-col gap-2 space-y-2">
<Label>Publish Target</Label>
<div className="flex gap-2 items-center">
<Checkbox
id="5"
id="4"
checked={selectedPublishers.includes(5)}
onChange={() => handleCheckboxChange(5)}
className="border"
/>
<Label htmlFor="5">UMUM</Label>
</div>
<div className="flex gap-2 items-center">
<Checkbox
id="6"
id="5"
checked={selectedPublishers.includes(6)}
onChange={() => handleCheckboxChange(6)}
className="border"
/>
<Label htmlFor="6">JOURNALIS</Label>
</div>
<div className="flex gap-2 items-center">
<Checkbox
id="7"
checked={selectedPublishers.includes(7)}
onChange={() => handleCheckboxChange(7)}
/>
<Label htmlFor="7">POLRI</Label>
</div>
<div className="flex gap-2 items-center">
<Checkbox
id="8"
checked={selectedPublishers.includes(8)}
onChange={() => handleCheckboxChange(8)}
/>
<Label htmlFor="8">KSP</Label>
</div>
</div>
</div>
<SuggestionModal
@ -821,10 +817,10 @@ export default function FormTeksDetail() {
</DialogContent>
</Dialog>
</Card>
{Number(detail?.needApprovalFromLevel) == Number(userLevelId) ? (
{/* {Number(detail?.needApprovalFromLevel) == Number(userLevelId) ? (
Number(detail?.uploadedById) == Number(userId) ? (
""
) : (
) : ( */}
<div className="flex flex-col gap-2 p-3">
<Button
onClick={() => actionApproval("2")}
@ -849,10 +845,10 @@ export default function FormTeksDetail() {
Reject
</Button>
</div>
)
{/* )
) : (
""
)}
)} */}
</div>
</div>
) : (

View File

@ -144,10 +144,8 @@ export default function FormTeks() {
const [publishedFor, setPublishedFor] = useState<string[]>([]);
const options: Option[] = [
{ id: "all", label: "SEMUA" },
{ id: "5", label: "UMUM" },
{ id: "6", label: "JOURNALIS" },
{ id: "7", label: "POLRI" },
{ id: "8", label: "KSP" },
{ id: "4", label: "UMUM" },
{ id: "5", label: "JOURNALIS" },
];
const { getRootProps, getInputProps } = useDropzone({
@ -638,12 +636,11 @@ export default function FormTeks() {
}
if (id == undefined) {
// New Articles API request data structure
const articleData: CreateArticleData = {
aiArticleId: 0, // default 0
aiArticleId: 0,
categoryIds: selectedCategory.toString(),
createdAt: formatDateForBackend(new Date()), // ✅ format sesuai backend
createdById: Number(userId), // isi dengan userId valid
createdAt: formatDateForBackend(new Date()),
createdById: Number(userId),
description: htmlToString(finalDescription),
htmlDescription: finalDescription,
isDraft: true,
@ -655,9 +652,8 @@ export default function FormTeks() {
.replace(/[^a-z0-9-]/g, ""),
tags: finalTags,
title: finalTitle,
typeId: 1, // Image content type
typeId: 3,
};
// Use new Articles API
const response = await createArticle(articleData);
console.log("Article Data Submitted:", articleData);
console.log("Article API Response:", response);
@ -670,16 +666,12 @@ export default function FormTeks() {
);
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
const formData = new FormData();
// Add all files to FormData
files.forEach((file, index) => {
formData.append("files", file);
});
@ -701,10 +693,9 @@ export default function FormTeks() {
console.log("Files uploaded successfully:", uploadResponse);
// Upload thumbnail using first file as thumbnail
if (files.length > 0) {
const thumbnailFormData = new FormData();
thumbnailFormData.append("files", files[0]); // Use first file as thumbnail
thumbnailFormData.append("files", files[0]);
console.log("Uploading thumbnail for article:", articleId);
@ -719,7 +710,6 @@ export default function FormTeks() {
"Thumbnail upload failed:",
thumbnailResponse.message
);
// Don't fail the whole process if thumbnail upload fails
} else {
console.log(
"Thumbnail uploaded successfully:",
@ -728,7 +718,6 @@ export default function FormTeks() {
}
} catch (thumbnailError) {
console.warn("Thumbnail upload error:", thumbnailError);
// Don't fail the whole process if thumbnail upload fails
}
}
} catch (uploadError) {
@ -741,7 +730,6 @@ export default function FormTeks() {
return false;
}
// Show success message
MySwal.fire({
title: "Sukses",
text: "Article dan files berhasil disimpan.",
@ -749,7 +737,7 @@ export default function FormTeks() {
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then(() => {
router.push("/admin/content/document");
router.push("/admin/content/text");
});
Cookies.remove("idCreate");
@ -980,8 +968,8 @@ export default function FormTeks() {
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex flex-col lg:flex-row gap-10">
<Card className="w-full lg:w-8/12">
<div className="flex flex-col lg:flex-row gap-10 border rounded-lg">
<Card className="w-full lg:w-8/12 m-2">
<div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">Form Document</p>
<div className="gap-5 mb-5">
@ -1428,7 +1416,7 @@ export default function FormTeks() {
{/* Submit Button */}
</div>
</Card>
<div className="w-full lg:w-4/12">
<div className="w-full lg:w-4/12 m-2">
<Card className=" h-[500px]">
<div className="px-3 py-3">
<div className="space-y-2">
@ -1561,6 +1549,7 @@ export default function FormTeks() {
id={option.id}
checked={isChecked}
onCheckedChange={handleChange}
className="border"
/>
<Label htmlFor={option.id}>{option.label}</Label>
</div>

View File

@ -2,8 +2,9 @@ import React from "react";
type FileData = {
url: string;
format: string; // extension with dot, e.g. ".pdf"
format: string; // bisa ".pdf" atau "mp4" atau "image/jpeg"
fileName?: string;
type?: string; // optional dari API (misal: video/mp4)
};
interface FilePreviewProps {
@ -11,48 +12,102 @@ interface FilePreviewProps {
}
const FileTextPreview: React.FC<FilePreviewProps> = ({ file }) => {
const format = file.format.toLowerCase();
const format = (file.format || "").toLowerCase();
const type = (file.type || "").toLowerCase();
if ([".jpg", ".jpeg", ".png", ".webp"].includes(format)) {
// 🖼️ Gambar
if (
[".jpg", ".jpeg", ".png", ".webp", ".gif"].some((ext) =>
format.includes(ext)
) ||
type.includes("image")
) {
return (
<img
className="object-fill h-full w-full rounded-md"
className="object-contain h-[500px] w-full rounded-md bg-black"
src={file.url}
alt={file.fileName || "File"}
alt={file.fileName || "Image File"}
/>
);
}
if (format === ".pdf") {
// 🎬 Video
if (
["mp4", "mov", "avi", ".mp4", ".mov", ".avi"].some((ext) =>
format.includes(ext)
) ||
type.includes("video")
) {
return (
<video
className="object-contain h-[500px] w-full rounded-md bg-black"
controls
>
<source src={file.url} type={type || "video/mp4"} />
Browser Anda tidak mendukung video tag.
</video>
);
}
// 🎧 Audio
if (
["mp3", "wav", "ogg", ".mp3", ".wav", ".ogg"].some((ext) =>
format.includes(ext)
) ||
type.includes("audio")
) {
return (
<div className="flex flex-col items-center justify-center h-[200px] bg-gray-100 rounded-md">
<audio controls className="w-full max-w-[90%]">
<source src={file.url} type={type || "audio/mpeg"} />
Browser Anda tidak mendukung audio tag.
</audio>
</div>
);
}
// 📄 PDF
if (format.includes(".pdf") || type.includes("pdf")) {
return (
<iframe
className="w-full h-96 rounded-md"
src={`https://drive.google.com/viewerng/viewer?embedded=true&url=${encodeURIComponent(file.url)}`}
className="w-full h-[600px] rounded-md"
src={`https://drive.google.com/viewerng/viewer?embedded=true&url=${encodeURIComponent(
file.url
)}`}
title={file.fileName || "PDF File"}
/>
);
}
if ([".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx"].includes(format)) {
// 🧾 Dokumen Office
if (
[".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx"].some((ext) =>
format.includes(ext)
)
) {
return (
<iframe
className="w-full h-96 rounded-md"
src={`https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(file.url)}`}
title={file.fileName || "Document"}
className="w-full h-[600px] rounded-md"
src={`https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(
file.url
)}`}
title={file.fileName || "Office Document"}
/>
);
}
// Fallback → unknown format
// 🔗 Default fallback → link download
return (
<a
href={file.url}
target="_blank"
rel="noopener noreferrer"
className="block text-blue-500 underline"
>
View {file.fileName || "File"}
</a>
<div className="text-center py-20">
<a
href={file.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 underline"
>
Lihat {file.fileName || "File"}
</a>
</div>
);
};

View File

@ -117,32 +117,52 @@ export default function MediaUpdate() {
}
}
// if (contentType === "all") {
// setFilteredData(dataToRender);
// return;
// }
// Function to filter data by content type
// function filterDataByContentType() {
// const filtered = dataToRender.filter((item) => {
// // Determine content type based on item properties
// const hasVideo = item.videoUrl || item.videoPath;
// const hasAudio = item.audioUrl || item.audioPath;
// const hasImage =
// item.smallThumbnailLink || item.thumbnailUrl || item.imageUrl;
// const hasText = item.content || item.description;
// switch (contentType) {
// case "audiovisual":
// return hasVideo;
// case "audio":
// return hasAudio && !hasVideo;
// case "foto":
// return hasImage && !hasVideo && !hasAudio;
// case "text":
// return hasText && !hasVideo && !hasAudio && !hasImage;
// default:
// return true;
// }
// });
// setFilteredData(filtered);
// }
function filterDataByContentType() {
// if (contentType === "all") {
// setFilteredData(dataToRender);
// return;
// }
const filtered = dataToRender.filter((item) => {
// Determine content type based on item properties
const hasVideo = item.videoUrl || item.videoPath;
const hasAudio = item.audioUrl || item.audioPath;
const hasImage =
item.smallThumbnailLink || item.thumbnailUrl || item.imageUrl;
const hasText = item.content || item.description;
switch (contentType) {
case "audiovisual":
return hasVideo && (hasAudio || hasImage);
return item.typeId === 2; // Video
case "audio":
return hasAudio && !hasVideo;
return item.typeId === 4; // Audio
case "foto":
return hasImage && !hasVideo && !hasAudio;
return item.typeId === 1; // Image
case "text":
return hasText && !hasVideo && !hasAudio && !hasImage;
return item.typeId === 3; // Text
default:
return true;
return true; // Semua jenis
}
});