This commit is contained in:
Sabda Yagra 2026-02-13 19:44:21 +07:00
commit 11d47e4c29
15 changed files with 1046 additions and 292 deletions

View File

@ -427,14 +427,14 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
helpText="Group classification for organization"
/>
<FormField
{/* <FormField
label="Is Approval Active"
name={`isApprovalActive-${index}`}
type="checkbox"
value={item.isApprovalActive}
onChange={(value) => onUpdate({ ...item, isApprovalActive: value })}
helpText="Users with this level can participate in approval process"
/>
/> */}
<FormField
label="Is Active"
@ -1152,7 +1152,7 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
{/* <FormField
label="Is Approval Active"
name="isApprovalActive"
type="checkbox"
@ -1161,7 +1161,7 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
handleFieldChange("isApprovalActive", value)
}
helpText="Users with this level can participate in approval process"
/>
/> */}
<FormField
label="Is Active"

View File

@ -31,6 +31,7 @@ type CategoryDetail = {
isActive: boolean;
createdAt: string;
updatedAt: string;
createdByFullname: string;
};
export default function CategoriesUpdateForm() {
@ -257,8 +258,9 @@ export default function CategoriesUpdateForm() {
<div>
<Label>Created By</Label>
<Input
disabled
type="text"
value={formData.createdByName || formData.createdById}
value={formData.createdByFullname}
readOnly
/>
</div>

View File

@ -94,6 +94,7 @@ type Detail = {
createdAt: string;
updatedAt: string;
files: FileType[] | null;
publishedFor?: string | null;
categories: {
id: number;
title: string;
@ -161,6 +162,16 @@ export default function FormVideoDetail() {
fetchCategories();
}, []);
useEffect(() => {
if (!detail?.publishedFor) return;
const publisherIds = detail.publishedFor
.split(",")
.map((id) => Number(id.trim()));
setSelectedPublishers(publisherIds);
}, [detail]);
useEffect(() => {
async function fetchDetail() {
if (!id) return;
@ -175,9 +186,22 @@ export default function FormVideoDetail() {
uploadedById: details?.createdById,
files: details?.files || [],
thumbnailUrl: details?.thumbnailUrl || details?.thumbnail || "",
publishedFor: details?.publishedFor,
};
setDetail(mappedDetail);
setFiles(details?.files || []);
// 🔥 Parse publish target seperti content image
const rawPublished =
details?.published_for || details?.publishedFor || "";
if (rawPublished) {
const publisherIds = rawPublished
.split(",")
.map((id: string) => Number(id.trim()));
setSelectedPublishers(publisherIds);
}
const approvals = await getDataApprovalByMediaUpload(mappedDetail.id);
setApproval(approvals?.data?.data);
} catch (err) {
@ -359,7 +383,60 @@ export default function FormVideoDetail() {
</div>
</div>
<div className="px-3 py-3 gap-2">
<div className="px-3 py-3">
<div className="flex flex-col gap-2 space-y-2">
<Label>Publish Target</Label>
{/* UMUM = 4 */}
<div className="flex gap-2 items-center">
<input
type="checkbox"
id="4"
value="4"
checked={selectedPublishers.includes(4)}
readOnly
className="h-4 w-4 border border-gray-300 rounded"
/>
<Label htmlFor="4">UMUM</Label>
</div>
{/* JOURNALIS = 5 */}
<div className="flex gap-2 items-center">
<input
type="checkbox"
id="5"
value="5"
checked={selectedPublishers.includes(5)}
readOnly
className="h-4 w-4 border border-gray-300 rounded"
/>
<Label htmlFor="5">JOURNALIS</Label>
</div>
</div>
{/* <div className="flex flex-col gap-2 space-y-2">
<Label>Publish Target</Label>
<div className="flex gap-2 items-center">
<Checkbox
id="4"
checked={selectedPublishers.includes(5)}
disabled
/>
<Label htmlFor="4">UMUM</Label>
</div>
<div className="flex gap-2 items-center">
<Checkbox
id="5"
checked={selectedPublishers.includes(6)}
disabled
/>
<Label htmlFor="5">JOURNALIS</Label>
</div>
</div> */}
</div>
{/* <div className="px-3 py-3 gap-2">
<Label>Publish Target</Label>
{[5, 6].map((target) => (
<div key={target} className="flex items-center gap-2">
@ -374,7 +451,7 @@ export default function FormVideoDetail() {
</Label>
</div>
))}
</div>
</div> */}
<div className="px-3 py-3 border mx-3">
<p>Information:</p>

View File

@ -379,7 +379,7 @@ type Option = {
label: string;
};
export default function FormVideo() {
export default function FormVideo() {
const MySwal = withReactContent(Swal);
const router = useRouter();
const editor = useRef(null);
@ -930,12 +930,11 @@ export default function FormVideo() {
}
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,
@ -948,9 +947,9 @@ export default function FormVideo() {
tags: finalTags,
title: finalTitle,
typeId: 2,
publishedFor: data.publishedFor.join(","),
};
// Use new Articles API
const response = await createArticle(articleData);
console.log("Article Data Submitted:", articleData);
console.log("Article API Response:", response);
@ -1651,7 +1650,8 @@ export default function FormVideo() {
<p className="text-sm font-semibold">Content Rewrite</p>
<div className="my-2">
<button type="button"
<button
type="button"
onClick={handleRewriteClick}
className="bg-blue-500 text-white py-2 px-4 rounded"
>

View File

@ -177,7 +177,13 @@ export default function FormVideoUpdate() {
setSelectedCategory(String(detailData.categories[0].id));
setTags(detailData.tags?.split(",").map((t: string) => t.trim()) || []);
setPublishedFor(detailData.publishedFor?.split(",") || []);
// setPublishedFor(detailData.publishedFor?.split(",") || []);
const publishTargets = detailData?.publishedFor
? detailData.publishedFor.split(",")
: [];
setPublishedFor(publishTargets);
setValue("publishedFor", publishTargets);
} catch (err) {
close();
console.error("❌ Error loading detail:", err);
@ -218,6 +224,7 @@ export default function FormVideoUpdate() {
title: data.title,
typeId: detail.typeId,
slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"),
publishedFor: data.publishedFor.join(","),
};
// const payload = {
@ -282,6 +289,17 @@ export default function FormVideoUpdate() {
if (!detail) return <p className="p-5 text-center">Memuat data...</p>;
const getVideoSrc = (url?: string) => {
if (!url) return "";
// kalau sudah absolute
if (url.startsWith("http")) return url;
// kalau backend kirim relative path
const baseUrl = process.env.NEXT_PUBLIC_API_URL || "";
return `${baseUrl}${url}`;
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex flex-col lg:flex-row gap-10 border rounded-lg">
@ -363,21 +381,24 @@ export default function FormVideoUpdate() {
{detailFiles.map((file) => (
<div key={file.id} className="flex flex-row items-center gap-4">
<video
className="object-contain w-[300px] h-[200px]"
src={file.fileUrl}
className="object-contain w-[300px] h-[200px] rounded border"
src={getVideoSrc(file.fileUrl)}
controls
preload="metadata"
title={file.fileName}
/>
<p>{file.fileName}</p>
<a
<button
type="button"
className="text-destructive"
onClick={() => handleDeleteFile(file.id)}
>
<TimesIcon />
</a>
</button>
</div>
))}
{files?.map((file) => (
<div
key={file.name}
@ -466,6 +487,68 @@ export default function FormVideoUpdate() {
<div className="mt-4 space-y-2">
<Label>Publish Target</Label>
<Controller
control={control}
name="publishedFor"
render={({ field }) => {
const isAllChecked =
field.value?.length ===
options.filter((opt) => opt.id !== "all").length;
return (
<div className="flex flex-col gap-3">
{options.map((option) => {
const isChecked =
option.id === "all"
? isAllChecked
: field.value?.includes(option.id);
const handleChange = (checked: boolean) => {
let updated: string[] = [];
if (option.id === "all") {
updated = checked
? options
.filter((opt) => opt.id !== "all")
.map((opt) => opt.id)
: [];
} else {
updated = checked
? [...(field.value || []), option.id]
: field.value?.filter(
(val) => val !== option.id,
) || [];
}
field.onChange(updated);
setPublishedFor(updated);
};
return (
<div
key={option.id}
className="flex gap-2 items-center"
>
<input
type="checkbox"
id={option.id}
checked={isChecked}
onChange={(e) => handleChange(e.target.checked)}
/>
<Label htmlFor={option.id}>{option.label}</Label>
</div>
);
})}
{errors.publishedFor && (
<p className="text-red-500 text-sm">
{errors.publishedFor.message}
</p>
)}
</div>
);
}}
/>
{/* <Controller
control={control}
name="publishedFor"
render={({ field }) => (
@ -512,13 +595,6 @@ export default function FormVideoUpdate() {
onChange={(e) => handleChange(e.target.checked)}
className="border"
/>
{/* <Checkbox
id={option.id}
checked={isChecked}
onCheckedChange={handleChange}
className="border"
/> */}
<Label htmlFor={option.id}>{option.label}</Label>
</div>
);
@ -532,7 +608,7 @@ export default function FormVideoUpdate() {
</div>
</div>
)}
/>
/> */}
</div>
<div className="flex justify-end gap-3 mt-5">

View File

@ -91,25 +91,63 @@ type FileType = {
};
type Detail = {
id: string;
id: number;
title: string;
description: string;
htmlDescription: string;
slug: string;
category: {
categoryId: number;
categoryName: string;
typeId: number;
tags: string;
thumbnailUrl: string;
pageUrl: string | null;
createdById: number;
createdByName: string;
shareCount: number;
viewCount: number;
commentCount: number;
aiArticleId: number | null;
oldId: number;
statusId: number;
isBanner: boolean;
isPublish: boolean;
publishedAt: string | null;
isActive: boolean;
createdAt: string;
updatedAt: string;
files: FileType[] | null;
publishedFor?: string | null;
categories: {
id: number;
title: string;
description: string;
thumbnailUrl: string;
slug: string | null;
tags: string[];
thumbnailPath: string | null;
parentId: number;
oldCategoryId: number | null;
createdById: number;
statusId: number;
isPublish: boolean;
publishedAt: string | null;
isEnabled: boolean | null;
isActive: boolean;
createdAt: string;
updatedAt: string;
}[];
// Legacy fields for backward compatibility
category?: {
id: number;
name: string;
};
categoryName: string;
creatorName: string;
thumbnailLink: string;
tags: string;
statusName: string;
isPublish: boolean;
needApprovalFromLevel: number;
files: FileType[];
uploadedById: number;
creatorName?: string;
thumbnailLink?: string;
statusName?: string;
needApprovalFromLevel?: number;
uploadedById?: number;
};
const ViewEditor = dynamic(
() => {
return import("@/components/editor/view-editor");
@ -219,6 +257,16 @@ export default function FormAudioDetail() {
}
}, [userLevelId, roleId]);
useEffect(() => {
if (!detail?.publishedFor) return;
const publisherIds = detail.publishedFor
.split(",")
.map((id: any) => Number(id.trim()));
setSelectedPublishers(publisherIds);
}, [detail]);
const handleCheckboxChange = (id: number) => {
setSelectedPublishers((prev) =>
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id],
@ -266,6 +314,18 @@ export default function FormAudioDetail() {
});
setupPlacementCheck(details?.files?.length);
// 🔥 Parse publish target seperti content image
const rawPublished =
details?.published_for || details?.publishedFor || "";
if (rawPublished) {
const publisherIds = rawPublished
.split(",")
.map((id: string) => Number(id.trim()));
setSelectedPublishers(publisherIds);
}
if (details?.publishedForObject) {
const publisherIds = details?.publishedForObject.map(
(obj: any) => obj.id,
@ -611,6 +671,58 @@ export default function FormAudioDetail() {
</div>
</div>
<div className="px-3 py-3">
<div className="flex flex-col gap-2 space-y-2">
<Label>Publish Target</Label>
{/* UMUM = 4 */}
<div className="flex gap-2 items-center">
<input
type="checkbox"
id="4"
value="4"
checked={selectedPublishers.includes(4)}
readOnly
className="h-4 w-4 border border-gray-300 rounded"
/>
<Label htmlFor="4">UMUM</Label>
</div>
{/* JOURNALIS = 5 */}
<div className="flex gap-2 items-center">
<input
type="checkbox"
id="5"
value="5"
checked={selectedPublishers.includes(5)}
readOnly
className="h-4 w-4 border border-gray-300 rounded"
/>
<Label htmlFor="5">JOURNALIS</Label>
</div>
</div>
{/* <div className="flex flex-col gap-2 space-y-2">
<Label>Publish Target</Label>
<div className="flex gap-2 items-center">
<Checkbox
id="4"
checked={selectedPublishers.includes(5)}
disabled
/>
<Label htmlFor="4">UMUM</Label>
</div>
<div className="flex gap-2 items-center">
<Checkbox
id="5"
checked={selectedPublishers.includes(6)}
disabled
/>
<Label htmlFor="5">JOURNALIS</Label>
</div>
</div> */}
</div>
{/* <div className="px-3 py-3">
<div className="flex flex-col gap-2 space-y-2">
<Label>Publish Target</Label>
<div className="flex gap-2 items-center">
@ -632,7 +744,7 @@ export default function FormAudioDetail() {
<Label htmlFor="6">JOURNALIS</Label>
</div>
</div>
</div>
</div> */}
<SuggestionModal
id={Number(id)}
numberOfSuggestion={detail?.numberOfSuggestion}

View File

@ -650,6 +650,7 @@ export default function FormAudio() {
tags: finalTags,
title: finalTitle,
typeId: 4,
publishedFor: data.publishedFor.join(","),
};
// Use new Articles API

View File

@ -250,6 +250,14 @@ export default function FormAudioUpdate() {
setTags(details.tags?.split(",").map((t: string) => t.trim()) || []);
setPublishedFor(details.publishedFor?.split(",") || []);
if (details?.publishedFor) {
const parsed = details.publishedFor
.split(",")
.map((id: string) => id.trim());
setValue("publishedFor", parsed); // 🔥 WAJIB
}
if (details?.files) {
setPrefFiles(details.files);
// setFiles(details.files);
@ -352,7 +360,7 @@ export default function FormAudioUpdate() {
// isYoutube: false,
// isInternationalMedia: false,
// };
const payload = {
aiArticleId: detail.aiArticleId,
categoryIds: selectedCategory,
@ -362,6 +370,7 @@ export default function FormAudioUpdate() {
title: data.title,
typeId: detail.typeId,
slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"),
publishedFor: data.publishedFor.join(","),
};
// const payload = {
// aiArticleId: detail?.aiArticleId ?? "",
@ -560,50 +569,87 @@ export default function FormAudioUpdate() {
setFiles([...filtered]);
};
const fileList = files.map((file: any) => (
<div
key={file.id} // Gunakan ID file sebagai key
className="flex justify-between border px-3.5 py-3 my-6 rounded-md"
>
<div className="flex gap-3 items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewBox="0 0 20 20"
>
<path
fill="currentColor"
d="M14.702 2.226A1 1 0 0 1 16 3.18v6.027a5.5 5.5 0 0 0-1-.184V6.18L8 8.368V15.5a2.5 2.5 0 1 1-1-2V5.368a1 1 0 0 1 .702-.955zM8 7.32l7-2.187V3.18L8 5.368zM5.5 14a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3m13.5.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0m-2.265-.436l-2.994-1.65a.5.5 0 0 0-.741.438v3.3a.5.5 0 0 0 .741.438l2.994-1.65a.5.5 0 0 0 0-.876"
/>
</svg>{" "}
<div>
<div className="text-sm text-card-foreground">
{file.fileName || file.name}
</div>
<div className="text-xs font-light text-muted-foreground">
{Math.round(file.size / 100) / 10 > 1000 ? (
<>{(Math.round(file.size / 100) / 10000).toFixed(1)}</>
) : (
<>{(Math.round(file.size / 100) / 10).toFixed(1)}</>
)}
{" kb"}
</div>
</div>
</div>
const getAudioUrl = (file: any) => {
if (file instanceof File) {
return URL.createObjectURL(file);
}
return file.secondaryUrl || file.fileUrl || null;
};
<Button
type="button"
size="icon"
color="destructive"
variant="outline"
className="border-none rounded-full"
onClick={() => handleRemoveFile(file)} // Kirim ID spesifik
const fileList = files.map((file: any) => {
const audioSrc = getAudioUrl(file);
return (
<div
key={file.id || file.name}
className="flex flex-col gap-2 border p-3 my-6 rounded-md"
>
<Icon icon="tabler:x" className="h-5 w-5" />
</Button>
</div>
));
<p className="text-sm font-medium truncate">{file.name}</p>
{audioSrc && (
<audio controls className="w-full">
<source src={audioSrc} type={file.type || "audio/mpeg"} />
Browser tidak mendukung audio.
</audio>
)}
<Button
type="button"
size="icon"
variant="outline"
className="self-end"
onClick={() => handleRemoveFile(file)}
>
<Icon icon="tabler:x" className="h-5 w-5" />
</Button>
</div>
);
});
// const fileList = files.map((file: any) => (
// <div
// key={file.id} // Gunakan ID file sebagai key
// className="flex justify-between border px-3.5 py-3 my-6 rounded-md"
// >
// <div className="flex gap-3 items-center">
// <svg
// xmlns="http://www.w3.org/2000/svg"
// width="48"
// height="48"
// viewBox="0 0 20 20"
// >
// <path
// fill="currentColor"
// d="M14.702 2.226A1 1 0 0 1 16 3.18v6.027a5.5 5.5 0 0 0-1-.184V6.18L8 8.368V15.5a2.5 2.5 0 1 1-1-2V5.368a1 1 0 0 1 .702-.955zM8 7.32l7-2.187V3.18L8 5.368zM5.5 14a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3m13.5.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0m-2.265-.436l-2.994-1.65a.5.5 0 0 0-.741.438v3.3a.5.5 0 0 0 .741.438l2.994-1.65a.5.5 0 0 0 0-.876"
// />
// </svg>{" "}
// <div>
// <div className="text-sm text-card-foreground">
// {file.fileName || file.name}
// </div>
// <div className="text-xs font-light text-muted-foreground">
// {Math.round(file.size / 100) / 10 > 1000 ? (
// <>{(Math.round(file.size / 100) / 10000).toFixed(1)}</>
// ) : (
// <>{(Math.round(file.size / 100) / 10).toFixed(1)}</>
// )}
// {" kb"}
// </div>
// </div>
// </div>
// <Button
// type="button"
// size="icon"
// color="destructive"
// variant="outline"
// className="border-none rounded-full"
// onClick={() => handleRemoveFile(file)} // Kirim ID spesifik
// >
// <Icon icon="tabler:x" className="h-5 w-5" />
// </Button>
// </div>
// ));
const handleCheckboxChangeImage = (fileId: number, value: string) => {
setSelectedOptions((prev: any) => {
@ -792,9 +838,45 @@ export default function FormAudioUpdate() {
</Fragment>
) : null}
{prevFiles?.length > 0 &&
prevFiles.map((file: any) => {
const audioSrc = file.secondaryUrl || file.fileUrl;
return (
<div
key={file.id}
className="flex flex-col gap-2 border p-3 my-6 rounded-md"
>
<p className="text-sm font-medium truncate">
{file.fileName}
</p>
{audioSrc ? (
<audio controls className="w-full">
<source src={audioSrc} type="audio/mpeg" />
Browser tidak mendukung audio.
</audio>
) : (
<p className="text-xs text-red-500">
Audio source tidak tersedia
</p>
)}
<Button
size="icon"
variant="outline"
className="self-end"
onClick={() => handleDeleteFile(file.id)}
>
<Icon icon="tabler:x" className="h-5 w-5" />
</Button>
</div>
);
})}
{/* {prevFiles?.length > 0 &&
prevFiles.map((file: any) => (
<div
key={file.id} // Gunakan ID file sebagai key
key={file.id}
className="flex justify-between border px-3.5 py-3 my-6 rounded-md"
>
<div className="flex gap-3 items-center">
@ -842,7 +924,7 @@ export default function FormAudioUpdate() {
<Icon icon="tabler:x" className="h-5 w-5" />
</Button>
</div>
))}
))} */}
{/* {files.length > 0 && (
<div className="mt-4">
<Label className="text-lg font-semibold">
@ -1012,6 +1094,70 @@ export default function FormAudioUpdate() {
<div className="mt-4 space-y-2">
<Label>Publish Target</Label>
<Controller
control={control}
name="publishedFor"
render={({ field }) => {
const isAllChecked =
field.value?.length ===
options.filter((opt) => opt.id !== "all").length;
return (
<div className="flex flex-col gap-3">
{options.map((option) => {
const isChecked =
option.id === "all"
? isAllChecked
: field.value?.includes(option.id);
const handleChange = (checked: boolean) => {
let updated: string[] = [];
if (option.id === "all") {
updated = checked
? options
.filter((opt) => opt.id !== "all")
.map((opt) => opt.id)
: [];
} else {
updated = checked
? [...(field.value || []), option.id]
: field.value?.filter(
(val) => val !== option.id,
) || [];
}
field.onChange(updated);
setPublishedFor(updated);
};
return (
<div
key={option.id}
className="flex gap-2 items-center"
>
<input
type="checkbox"
id={option.id}
checked={isChecked}
onChange={(e) =>
handleChange(e.target.checked)
}
/>
<Label htmlFor={option.id}>{option.name}</Label>
</div>
);
})}
{errors.publishedFor && (
<p className="text-red-500 text-sm">
{errors.publishedFor.message}
</p>
)}
</div>
);
}}
/>
{/* <Controller
control={control}
name="publishedFor"
render={({ field }) => (
@ -1079,7 +1225,7 @@ export default function FormAudioUpdate() {
</div>
</div>
)}
/>
/> */}
</div>
</div>
{/* <div className="px-3 py-3 flex flex-row items-center text-blue-500 gap-2 text-sm">

View File

@ -94,23 +94,62 @@ type FileType = {
};
type Detail = {
id: string;
id: number;
title: string;
description: string;
htmlDescription: string;
slug: string;
category: {
categoryId: number;
categoryName: string;
typeId: number;
tags: string;
thumbnailUrl: string;
pageUrl: string | null;
createdById: number;
createdByName: string;
shareCount: number;
viewCount: number;
commentCount: number;
aiArticleId: number | null;
oldId: number;
statusId: number;
isBanner: boolean;
isPublish: boolean;
publishedAt: string | null;
isActive: boolean;
createdAt: string;
updatedAt: string;
files: FileType[] | null;
publishedFor?: string | null;
categories: {
id: number;
title: string;
description: string;
thumbnailUrl: string;
slug: string | null;
tags: string[];
thumbnailPath: string | null;
parentId: number;
oldCategoryId: number | null;
createdById: number;
statusId: number;
isPublish: boolean;
publishedAt: string | null;
isEnabled: boolean | null;
isActive: boolean;
createdAt: string;
updatedAt: string;
}[];
// Legacy fields for backward compatibility
category?: {
id: number;
name: string;
};
categoryName: string;
creatorName: string;
thumbnailLink: string;
tags: string;
statusName: string;
isPublish: boolean;
needApprovalFromLevel: number;
files: FileType[];
uploadedById: number;
creatorName?: string;
thumbnailLink?: string;
statusName?: string;
needApprovalFromLevel?: number;
uploadedById?: number;
};
const ViewEditor = dynamic(
@ -208,6 +247,16 @@ export default function FormTeksDetail() {
initState();
}, []);
useEffect(() => {
if (!detail?.publishedFor) return;
const publisherIds = detail.publishedFor
.split(",")
.map((id: any) => Number(id.trim()));
setSelectedPublishers(publisherIds);
}, [detail]);
const getCategories = async () => {
try {
const categoryRes = await listArticleCategories(1, 100);
@ -244,6 +293,18 @@ export default function FormTeksDetail() {
setFiles(details?.files || []);
setDetail(details);
// 🔥 Parse publish target seperti content image
const rawPublished =
details?.published_for || details?.publishedFor || "";
if (rawPublished) {
const publisherIds = rawPublished
.split(",")
.map((id: string) => Number(id.trim()));
setSelectedPublishers(publisherIds);
}
// ✅ Aman untuk fileType
setMain({
type: details?.fileType?.name || "Unknown",
@ -563,6 +624,58 @@ export default function FormTeksDetail() {
</div>
</div>
<div className="px-3 py-3">
<div className="flex flex-col gap-2 space-y-2">
<Label>Publish Target</Label>
{/* UMUM = 4 */}
<div className="flex gap-2 items-center">
<input
type="checkbox"
id="4"
value="4"
checked={selectedPublishers.includes(4)}
readOnly
className="h-4 w-4 border border-gray-300 rounded"
/>
<Label htmlFor="4">UMUM</Label>
</div>
{/* JOURNALIS = 5 */}
<div className="flex gap-2 items-center">
<input
type="checkbox"
id="5"
value="5"
checked={selectedPublishers.includes(5)}
readOnly
className="h-4 w-4 border border-gray-300 rounded"
/>
<Label htmlFor="5">JOURNALIS</Label>
</div>
</div>
{/* <div className="flex flex-col gap-2 space-y-2">
<Label>Publish Target</Label>
<div className="flex gap-2 items-center">
<Checkbox
id="4"
checked={selectedPublishers.includes(5)}
disabled
/>
<Label htmlFor="4">UMUM</Label>
</div>
<div className="flex gap-2 items-center">
<Checkbox
id="5"
checked={selectedPublishers.includes(6)}
disabled
/>
<Label htmlFor="5">JOURNALIS</Label>
</div>
</div> */}
</div>
{/* <div className="px-3 py-3">
<div className="flex flex-col gap-2 space-y-2">
<Label>Publish Target</Label>
<div className="flex gap-2 items-center">
@ -584,7 +697,7 @@ export default function FormTeksDetail() {
<Label htmlFor="6">JOURNALIS</Label>
</div>
</div>
</div>
</div> */}
<SuggestionModal
id={Number(id)}
numberOfSuggestion={detail?.numberOfSuggestion}

View File

@ -653,6 +653,7 @@ export default function FormTeks() {
tags: finalTags,
title: finalTitle,
typeId: 3,
publishedFor: data.publishedFor.join(","),
};
const response = await createArticle(articleData);
console.log("Article Data Submitted:", articleData);

View File

@ -16,6 +16,7 @@ import * as z from "zod";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { useParams, useRouter } from "next/navigation";
import { v4 as uuidv4 } from "uuid";
import {
Select,
SelectContent,
@ -88,6 +89,12 @@ type Category = {
title: string;
};
interface DetailFile {
id: number;
fileUrl: string;
fileName: string;
}
type Detail = {
id: string;
title: string;
@ -110,6 +117,7 @@ type Detail = {
interface FileWithPreview extends File {
preview: string;
id: string;
}
type Option = {
@ -153,6 +161,7 @@ export default function FormTeksUpdate() {
[fileId: number]: string[];
}>({});
const [selectedTarget, setSelectedTarget] = useState("");
const [detailFiles, setDetailFiles] = useState<DetailFile[]>([]);
const [unitSelection, setUnitSelection] = useState({
allUnit: false,
mabes: false,
@ -161,18 +170,26 @@ export default function FormTeksUpdate() {
});
const [publishedFor, setPublishedFor] = useState<string[]>([]);
const inputRef = useRef<HTMLInputElement>(null);
const [existingFiles, setExistingFiles] = useState<DetailFile[]>([]);
let fileTypeId = "3";
const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) => {
setFiles(acceptedFiles.map((file) => Object.assign(file)));
setFiles(
acceptedFiles.map((file) =>
Object.assign(file, {
id: uuidv4(),
preview: URL.createObjectURL(file),
}),
),
);
},
accept: {
"application/pdf": [],
"application/msword": [], // .doc
"application/msword": [],
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
[], // .docx
[],
},
});
@ -219,9 +236,15 @@ export default function FormTeksUpdate() {
setValue("creatorName", details.createdByName ?? "");
setTags(details.tags?.split(",").map((t: string) => t.trim()) || []);
setPublishedFor(details.publishedFor?.split(",") || []);
if (details?.publishedFor) {
const publishArr = details.publishedFor.split(",");
setPublishedFor(publishArr); // state lokal
setValue("publishedFor", publishArr); // ← WAJIB untuk react-hook-form
}
if (details?.files) {
setFiles(details.files);
setExistingFiles(details.files);
const initialOptions: { [key: number]: string[] } = {};
details.files.forEach((file: any) => {
if (file.placements) {
@ -278,7 +301,6 @@ export default function FormTeksUpdate() {
return allSelected ? ["all", ...options] : options;
};
const handleCheckboxChange = (id: string) => {
if (id === "all") {
// Select all options except "all"
@ -326,6 +348,7 @@ export default function FormTeksUpdate() {
title: data.title,
typeId: detail.typeId,
slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"),
publishedFor: data.publishedFor.join(","),
};
// const payload = {
// aiArticleId: detail?.aiArticleId ?? "",
@ -679,152 +702,142 @@ export default function FormTeksUpdate() {
)}
</div>
<div className="py-3 space-y-2">
<Label>Select File</Label>
{/* <Input
id="fileInput"
type="file"
onChange={handleImageChange}
/> */}
<Fragment>
<div {...getRootProps({ className: "dropzone" })}>
<input {...getInputProps()} />
<div className=" w-full text-center border-dashed border border-default-200 dark:border-default-300 rounded-md py-[52px] flex items-center flex-col">
<CloudUpload className="text-default-300 w-10 h-10" />
<h4 className=" text-2xl font-medium mb-1 mt-3 text-card-foreground/80">
{/* Drop files here or click to upload. */}
Drag File
</h4>
<div className=" text-xs text-muted-foreground">
Upload File Text Max
<div className="py-3 space-y-2">
<Label>Select File</Label>
<Fragment>
<div {...getRootProps({ className: "dropzone" })}>
<input {...getInputProps()} />
<div className="w-full text-center border-dashed border border-black rounded-md py-[52px] flex items-center flex-col">
<CloudUpload className="w-10 h-10" />
<h4 className="text-2xl font-medium mt-3">
Drag File
</h4>
</div>
</div>
{/* 👇 TARUH DI SINI */}
{files.length > 0 && (
<div className="mt-4 space-y-2">
<Label className="text-lg font-semibold">
File Baru
</Label>
{files.map((file) => (
<div
key={file.name}
className="flex justify-between items-center border p-3 rounded-md"
>
<div>
<p className="font-medium">{file.name}</p>
<p className="text-xs text-muted-foreground">
{(file.size / 1024).toFixed(1)} KB
</p>
</div>
<Button
type="button"
size="icon"
variant="outline"
onClick={() =>
setFiles((prev) =>
prev.filter(
(item) => item.name !== file.name,
),
)
}
>
</Button>
</div>
))}
</div>
)}
</Fragment>
</div>
{/* {files.length > 0 && (
<div className="mt-4">
<Label className="text-md font-semibold">
File Media
</Label>
<div className="grid gap-4">
{files.map((file: any, index: number) => (
<div
key={file.id}
className="flex items-center border p-2 rounded-md"
>
{file.preview ? (
<img
src={file.preview}
alt={file.name}
className="w-16 h-16 object-cover rounded-md mr-4"
/>
) : (
<Icon
icon="tabler:file-description"
className="w-16 h-16"
/>
)}
<div className="flex-grow">
<p className="font-medium">
{file.fileName || file.name}
</p>
</div>
<Button
type="button"
variant="outline"
size="icon"
onClick={() =>
setFiles((prev) =>
prev.filter((f) => f.id !== file.id),
)
}
>
</Button>
</div>
))}
</div>
</div>
</div>
{files.length ? (
<Fragment>
<div>{fileList}</div>
<div className=" flex justify-between gap-2">
{/* <div className="flex flex-row items-center gap-3 py-3">
<Label>Watermark</Label>
<div className="flex items-center gap-3">
<Switch defaultChecked color="primary" id="c2" />
</div>
</div> */}
<Button
color="destructive"
onClick={handleRemoveAllFiles}
)} */}
{/* Existing Files */}
{existingFiles.length > 0 && (
<div className="mt-4 space-y-2">
<Label className="text-lg font-semibold">
File Sebelumnya
</Label>
{existingFiles.map((file) => (
<div
key={file.id}
className="flex justify-between items-center border p-3 rounded-md"
>
Remove All
</Button>
</div>
</Fragment>
) : null}
{files.length > 0 && (
<></>
// <div className="mt-4 space-y-2">
// <Label className="text-lg font-semibold">
// {" "}
// File Media
// </Label>
// <div className="grid gap-4">
// {files.map((file: any) => (
// <div
// key={file.id}
// className="flex items-center border p-2 rounded-md"
// >
// <img
// src={file.thumbnailFileUrl}
// alt={file.fileName}
// className="w-16 h-16 object-cover rounded-md mr-4"
// />
// <div className="flex flex-wrap gap-3 items-center ">
// <div className="flex-grow">
// <p className="font-medium">{file.fileName}</p>
// <a
// href={file.url}
// target="_blank"
// rel="noopener noreferrer"
// className="text-blue-500 text-sm"
// >
// View File
// </a>
// </div>
// <div>
// <Label className="flex items-center space-x-2">
// <input
// type="checkbox"
// checked={selectedOptions[
// file.id
// ]?.includes("all")}
// onChange={() =>
// handleCheckboxChangeImage(
// file.id,
// "all"
// )
// }
// className="form-checkbox"
// />
// <span>All</span>
// </Label>
// </div>
// <div>
// <Label className="flex items-center space-x-2">
// <input
// type="checkbox"
// checked={selectedOptions[
// file.id
// ]?.includes("nasional")}
// onChange={() =>
// handleCheckboxChangeImage(
// file.id,
// "nasional"
// )
// }
// className="form-checkbox"
// />
// <span>Nasional</span>
// </Label>
// </div>
// <div>
// <Label className="flex items-center space-x-2">
// <input
// type="checkbox"
// checked={selectedOptions[
// file.id
// ]?.includes("wilayah")}
// onChange={() =>
// handleCheckboxChangeImage(
// file.id,
// "wilayah"
// )
// }
// className="form-checkbox"
// />
// <span>Wilayah</span>
// </Label>
// </div>
// <div>
// <Label className="flex items-center space-x-2">
// <input
// type="checkbox"
// checked={selectedOptions[
// file.id
// ]?.includes("internasional")}
// onChange={() =>
// handleCheckboxChangeImage(
// file.id,
// "internasional"
// )
// }
// className="form-checkbox"
// />
// <span>Internasional</span>
// </Label>
// </div>
// </div>
// </div>
// ))}
// </div>
// </div>
<div className="flex items-center gap-3">
<Icon
icon="tabler:file-description"
className="w-10 h-10"
/>
<div>
<p className="font-medium">{file.fileName}</p>
<a
href={file.fileUrl}
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 text-sm"
>
Lihat File
</a>
</div>
</div>
</div>
))}
</div>
)}
</Fragment>
</div>
@ -852,11 +865,18 @@ export default function FormTeksUpdate() {
{/* <div className="mt-3 px-3">
<Label>Pratinjau Gambar Utama</Label>
<Card className="mt-2">
<img
src={detail.thumbnailLink}
alt="Thumbnail Gambar Utama"
className="w-full h-auto rounded"
/>
{files.preview ? (
<img
src={files.preview}
alt={files.name}
className="w-16 h-16 object-cover rounded-md mr-4"
/>
) : (
<Icon
icon="tabler:file-description"
className="w-16 h-16"
/>
)}
</Card>
</div> */}
<div className="px-3 py-3">
@ -898,6 +918,76 @@ export default function FormTeksUpdate() {
<div className="mt-4 space-y-2">
<Label>Publish Target</Label>
<Controller
control={control}
name="publishedFor"
render={({ field }) => {
const currentValue = field.value || [];
const isAllChecked =
currentValue.length ===
options.filter((opt) => opt.id !== "all").length;
return (
<div className="flex flex-col gap-3">
{options.map((option) => {
const isChecked =
option.id === "all"
? isAllChecked
: currentValue.includes(option.id);
const handleChange = (checked: boolean) => {
let updated: string[] = [];
if (option.id === "all") {
updated = checked
? options
.filter((opt) => opt.id !== "all")
.map((opt) => opt.id)
: [];
} else {
updated = checked
? [...currentValue, option.id]
: currentValue.filter(
(val) => val !== option.id,
);
}
field.onChange(updated);
setPublishedFor(updated);
};
return (
<div
key={option.id}
className="flex items-center gap-2"
>
<input
type="checkbox"
id={option.id}
checked={isChecked}
onChange={(e) =>
handleChange(e.target.checked)
}
/>
<Label htmlFor={option.id}>
{option.label}
</Label>
</div>
);
})}
{errors.publishedFor && (
<p className="text-red-500 text-sm">
{errors.publishedFor.message}
</p>
)}
</div>
);
}}
/>
{/* <Controller
control={control}
name="publishedFor"
render={({ field }) => (
@ -967,7 +1057,7 @@ export default function FormTeksUpdate() {
</div>
</div>
)}
/>
/> */}
</div>
</div>
<div className="px-3 py-3 flex flex-row items-center text-blue-500 gap-2 text-sm">

View File

@ -127,6 +127,7 @@ type Detail = {
createdAt: string;
updatedAt: string;
files: FileType[] | null;
published_for?: string;
categories: {
id: number;
title: string;
@ -322,6 +323,20 @@ export default function FormImageDetail() {
try {
const response = await getArticleDetail(Number(id));
const details = response?.data?.data;
console.log("DETAIL RESPONSE:", details);
// ===== PARSE published_for =====
const rawPublished =
details?.published_for || details?.publishedFor || "";
if (rawPublished) {
const publisherIds = rawPublished
.split(",")
.map((id: string) => Number(id.trim()));
setSelectedPublishers(publisherIds);
}
const mappedDetail: Detail = {
...details,
category:
@ -351,6 +366,14 @@ export default function FormImageDetail() {
setFiles(mappedFiles);
setDetail(mappedDetail);
if (details?.published_for) {
const publisherIds = details.published_for
.split(",")
.map((id: string) => Number(id.trim()));
setSelectedPublishers(publisherIds);
}
if (mappedFiles && mappedFiles.length > 0) {
setMain({
type: "image",
@ -369,13 +392,13 @@ export default function FormImageDetail() {
setDetailThumb(fileUrls);
if (details?.publishedForObject?.length > 0) {
const publisherIds = details.publishedForObject
.map((obj: any) => Number(obj.id))
.filter((id: number) => id === 5 || id === 6);
// if (details?.publishedForObject?.length > 0) {
// const publisherIds = details.publishedForObject
// .map((obj: any) => Number(obj.id))
// .filter((id: number) => id === 4 || id === 5);
setSelectedPublishers(publisherIds);
}
// setSelectedPublishers(publisherIds);
// }
const approvals = await getDataApprovalByMediaUpload(mappedDetail.id);
setApproval(approvals?.data?.data);
@ -773,36 +796,54 @@ export default function FormImageDetail() {
<div className="px-3 py-3">
<div className="flex flex-col gap-2 space-y-2">
<Label>Publish Target</Label>
{/* UMUM = 4 */}
<div className="flex gap-2 items-center">
{/* <Checkbox
<input
type="checkbox"
id="4"
value="4"
checked={selectedPublishers.includes(4)}
readOnly
className="h-4 w-4 border border-gray-300 rounded"
/>
<Label htmlFor="4">UMUM</Label>
</div>
{/* JOURNALIS = 5 */}
<div className="flex gap-2 items-center">
<input
type="checkbox"
id="5"
value="5"
checked={selectedPublishers.includes(5)}
onChange={() => handleCheckboxChange(5)}
className="border"
/> */}
readOnly
className="h-4 w-4 border border-gray-300 rounded"
/>
<Label htmlFor="5">JOURNALIS</Label>
</div>
</div>
{/* <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)}
disabled
/>
<Label htmlFor="5">UMUM</Label>
<Label htmlFor="4">UMUM</Label>
</div>
<div className="flex gap-2 items-center">
{/* <Checkbox
id="6"
checked={selectedPublishers.includes(6)}
onChange={() => handleCheckboxChange(6)}
className="border"
/> */}
<Checkbox
id="6"
id="5"
checked={selectedPublishers.includes(6)}
disabled
/>
<Label htmlFor="6">JOURNALIS</Label>
<Label htmlFor="5">JOURNALIS</Label>
</div>
</div>
</div> */}
</div>
<SuggestionModal

View File

@ -557,7 +557,7 @@ export default function FormImage() {
}
}, [articleBody, setValue]);
const userId = Cookies.get("userId"); // atau dari auth context / localStorage
const userId = Cookies.get("userId");
const save = async (data: ImageSchema) => {
loading();
@ -599,10 +599,10 @@ export default function FormImage() {
// ✅ Sesuaikan dengan struktur Swagger
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,
@ -614,9 +614,31 @@ export default function FormImage() {
.replace(/[^a-z0-9-]/g, ""),
tags: finalTags,
title: finalTitle,
typeId: 1, // Image content type
typeId: 1,
// 🔥 TAMBAHKAN INI
publishedFor: data.publishedFor.join(","),
};
// const articleData: CreateArticleData = {
// 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,
// isDraft: true,
// isPublish: false,
// oldId: 0,
// slug: finalTitle
// .toLowerCase()
// .replace(/\s+/g, "-")
// .replace(/[^a-z0-9-]/g, ""),
// tags: finalTags,
// title: finalTitle,
// typeId: 1, // Image content type
// };
let id = Cookies.get("idCreate");
if (id == undefined) {
@ -1526,7 +1548,7 @@ export default function FormImage() {
option.id === "all"
? isAllChecked
: field.value.includes(option.id);
const handleChange = (checked: boolean) => {
let updated: string[] = [];

View File

@ -59,7 +59,7 @@ import { useTranslations } from "next-intl";
const CustomEditor = dynamic(
() => import("@/components/editor/custom-editor"),
{ ssr: false }
{ ssr: false },
);
const imageSchema = z.object({
@ -99,7 +99,7 @@ export default function FormImageUpdate() {
setFiles((prev) => [
...prev,
...acceptedFiles.map((f) =>
Object.assign(f, { id: uuidv4(), preview: URL.createObjectURL(f) })
Object.assign(f, { id: uuidv4(), preview: URL.createObjectURL(f) }),
),
]),
accept: { "image/*": [] },
@ -171,12 +171,13 @@ export default function FormImageUpdate() {
const allOptions = options
.filter((opt) => opt.id !== "all")
.map((opt) => opt.id);
setPublishedFor(
publishedFor.length === allOptions.length ? [] : allOptions
publishedFor.length === allOptions.length ? [] : allOptions,
);
} else {
setPublishedFor((prev) =>
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id],
);
}
};
@ -233,6 +234,23 @@ export default function FormImageUpdate() {
// router.push("/admin/content/image");
// });
// };
const formatDateTime = (date: Date) => {
const pad = (n: number) => n.toString().padStart(2, "0");
return (
date.getFullYear() +
"-" +
pad(date.getMonth() + 1) +
"-" +
pad(date.getDate()) +
" " +
pad(date.getHours()) +
":" +
pad(date.getMinutes()) +
":" +
pad(date.getSeconds())
);
};
// 🔹 ganti fungsi save di FormImageUpdate.tsx
const save = async (data: ImageSchema) => {
@ -248,7 +266,10 @@ export default function FormImageUpdate() {
const payload = {
aiArticleId: detail?.aiArticleId ?? null,
categoryIds: selectedTarget ? String(selectedTarget) : "",
createdAt: detail?.createdAt ?? new Date().toISOString(),
// createdAt: detail?.createdAt ?? new Date().toISOString(),
createdAt: detail?.createdAt
? detail.createdAt.replace("T", " ").split("+")[0]
: formatDateTime(new Date()),
createdById: detail?.createdById ?? null,
description: htmlToString(descFinal),
htmlDescription: descFinal,
@ -261,9 +282,9 @@ export default function FormImageUpdate() {
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)+/g, ""),
statusId: detail?.statusId ?? 1,
tags: tags,
tags: tags.join(", "),
title: data.title,
typeId: 1, // 1 = image (sesuai struktur kamu)
typeId: 1,
};
console.log("📤 Payload Update Article:", payload);
@ -379,14 +400,14 @@ export default function FormImageUpdate() {
!categories?.find(
(cat) =>
String(cat.id) ===
String(detail.categoryId || detail?.category?.id)
String(detail.categoryId || detail?.category?.id),
) && (
<SelectItem
key={String(
detail.categoryId || detail?.category?.id
detail.categoryId || detail?.category?.id,
)}
value={String(
detail.categoryId || detail?.category?.id
detail.categoryId || detail?.category?.id,
)}
>
{detail.categoryName || detail?.category?.name}
@ -447,7 +468,7 @@ export default function FormImageUpdate() {
className="flex justify-between border p-3 rounded-md"
>
<div className="flex gap-3 items-center">
<Image
{/* <Image
src={
file.thumbnailFileUrl ||
file.preview ||
@ -457,11 +478,25 @@ export default function FormImageUpdate() {
width={64}
height={64}
className="rounded border"
/> */}
<Image
src={
file.preview || // file baru (dropzone)
file.thumbnailUrl || // dari backend jika ada
file.fileUrl || // fallback utama dari backend
"/placeholder.png"
}
alt={file.fileName || file.name || "file"}
width={64}
height={64}
className="rounded border object-cover"
unoptimized
/>
<div>
<p className="font-medium">{file.fileName}</p>
<a
href={file.url}
href={file.fileUrl || file.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 text-sm"
@ -554,6 +589,36 @@ export default function FormImageUpdate() {
</div>
<div>
<Label>Publish Target</Label>
<div className="flex flex-col gap-2 mt-2">
{options.map((opt) => {
const isAllSelected =
publishedFor.length ===
options.filter((o) => o.id !== "all").length;
const isChecked =
opt.id === "all"
? isAllSelected
: publishedFor.includes(opt.id);
return (
<div key={opt.id} className="flex items-center gap-2">
<input
type="checkbox"
id={opt.id}
value={opt.id}
checked={isChecked}
onChange={() => handleCheckboxChange(opt.id)}
className="w-4 h-4 accent-black"
/>
<Label htmlFor={opt.id}>{opt.name}</Label>
</div>
);
})}
</div>
</div>
{/* <div>
<Label>Publish Target</Label>
<div className="flex flex-col gap-2 mt-2">
{options.map((opt) => (
@ -572,12 +637,19 @@ export default function FormImageUpdate() {
</div>
))}
</div>
</div>
</div> */}
</Card>
<div className="flex justify-end gap-3 mt-4">
<Button type="submit">Simpan</Button>
<Button
variant="outline"
type="submit"
className="hover:bg-gray-300"
>
Simpan
</Button>
<Button
className="hover:bg-gray-300"
type="button"
variant="outline"
onClick={() => router.back()}

View File

@ -58,6 +58,7 @@ export interface CreateArticleData {
tags: string;
title: string;
typeId: number;
publishedFor: string;
}
// Interface for Article Category