diff --git a/app/(admin)/admin/news-article/image/update/[id]/page.tsx b/app/(admin)/admin/news-article/image/update/[id]/page.tsx new file mode 100644 index 0000000..a8dcaba --- /dev/null +++ b/app/(admin)/admin/news-article/image/update/[id]/page.tsx @@ -0,0 +1,11 @@ +import EditArticleForm from "@/components/form/article/edit-article-form"; + +export default function DetailArticlePage() { + return ( +
+
+ +
+
+ ); +} diff --git a/app/(admin)/admin/news-article/update/[id]/page.tsx b/app/(admin)/admin/news-article/update/[id]/page.tsx new file mode 100644 index 0000000..dacb161 --- /dev/null +++ b/app/(admin)/admin/news-article/update/[id]/page.tsx @@ -0,0 +1,11 @@ +import EditArticleForm from "@/components/form/article/edit-article-form"; + +export default function UpdateArticlePage() { + return ( +
+
+ +
+
+ ); +} diff --git a/components/editor/view-editor.js b/components/editor/view-editor.js index 1e579b0..f1cf28c 100644 --- a/components/editor/view-editor.js +++ b/components/editor/view-editor.js @@ -54,7 +54,7 @@ function ViewEditor(props) { } .ckeditor-view-wrapper :global(.ck.ck-editor__main) { - min-height: ${props.height || 400}px; + // min-height: ${props.height || 400}px; max-height: ${maxHeight}px; } diff --git a/components/form/article/edit-article-form.tsx b/components/form/article/edit-article-form.tsx index 6ff6933..5de8bc7 100644 --- a/components/form/article/edit-article-form.tsx +++ b/components/form/article/edit-article-form.tsx @@ -18,10 +18,7 @@ import { useParams, useRouter } from "next/navigation"; import GetSeoScore from "./get-seo-score-form"; import Link from "next/link"; import Cookies from "js-cookie"; -import { - isApproverOrAdmin, - isContributorRole, -} from "@/constants/user-roles"; +import { isApproverOrAdmin, isContributorRole } from "@/constants/user-roles"; import { createArticleSchedule, deleteArticleFiles, @@ -107,7 +104,7 @@ const createArticleSchema = z.object({ message: "Deskripsi harus diisi", }), category: z.array(categorySchema), - tags: z.array(z.string()).nonempty({ + tags: z.array(z.string()).min(1, { message: "Minimal 1 tag", }), source: z.enum(["internal", "external"]).optional(), @@ -162,6 +159,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) { const [isScheduled, setIsScheduled] = useState(false); const [startDateValue, setStartDateValue] = useState(); const [startTimeValue, setStartTimeValue] = useState(""); + const [openHistory, setOpenHistory] = useState(false); const [levelId, setLevelId] = useState(); @@ -184,7 +182,14 @@ export default function EditArticleForm(props: { isDetail: boolean }) { const formOptions = { resolver: zodResolver(createArticleSchema), - defaultValues: { title: "", description: "", category: [], tags: [], slug: "", customCreatorName: "" }, + defaultValues: { + title: "", + description: "", + category: [], + tags: [], + slug: "", + customCreatorName: "", + }, }; type UserSettingSchema = z.infer; const { @@ -256,7 +261,10 @@ export default function EditArticleForm(props: { isDetail: boolean }) { temp.push(datas[0]); } } - setValue("category", temp.length ? (temp as [CategoryType, ...CategoryType[]]) : []); + setValue( + "category", + temp.length ? (temp as [CategoryType, ...CategoryType[]]) : [], + ); }; useEffect(() => { @@ -328,9 +336,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) { title: detailData?.title, typeId: detailData?.typeId ?? 1, slug: detailData?.slug, - categoryIds: (getValues("category") ?? []) - .map((val) => val.id) - .join(","), + categoryIds: (getValues("category") ?? []).map((val) => val.id).join(","), tags: getValues("tags").join(","), description: htmlToString(getValues("description")), htmlDescription: getValues("description"), @@ -372,9 +378,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) { title: detailData?.title, typeId: detailData?.typeId ?? 1, slug: detailData?.slug, - categoryIds: (getValues("category") ?? []) - .map((val) => val.id) - .join(","), + categoryIds: (getValues("category") ?? []).map((val) => val.id).join(","), tags: getValues("tags").join(","), description: htmlToString(getValues("description")), htmlDescription: getValues("description"), @@ -685,81 +689,99 @@ export default function EditArticleForm(props: { isDetail: boolean }) { }; if (isDetail) { + const tags = + detailData?.tags + ?.split(",") + .map((t: string) => t.replace("#", "").trim()) ?? []; + return ( -
+
+ {/* 🔹 BREADCRUMB +
+ News & Articles + › + Detail Article +
*/} +
- {/* ================= LEFT SIDE ================= */} -
+ {/* ================= LEFT ================= */} +
{/* Title */} -
-

Title

-
+
+

Title

+
{detailData?.title}
- {/* Content type (articles.type_id) */} -
-

Content type

+ {/* Category */} +
+

Category

- {detailData?.typeId === 1 && "Image"} - {detailData?.typeId === 2 && "Text"} - {detailData?.typeId === 3 && "Video"} - {detailData?.typeId === 4 && "Audio"} - {![1, 2, 3, 4].includes(detailData?.typeId) && (detailData?.typeId ?? "—")} + {detailData?.categories?.length > 0 + ? detailData.categories + .map((cat: any) => cat.title) + .join(", ") + : "—"}
{/* Description */}
-

Description

-
- -
+

Description

+ +
- {/* Media File */} -
-

Media File

+ {/* Media */} +
+

Media File

- {detailfiles?.length > 0 ? ( + {detailData?.files?.length > 0 ? ( <> media -
- {detailfiles.map((file: any, index: number) => ( +
+ {detailData.files.map((file: any, i: number) => ( preview ))}
) : ( -

Belum ada file

+

No media

)}
- {/* ================= RIGHT SIDE ================= */} -
- {/* Meta & Thumbnail Card */} -
+ {/* ================= RIGHT ================= */} +
+
+ {/* Creator */} +
+

Creator

+
+ {detailData?.createdByName || "-"} +
+
+ {/* Thumbnail */}

Thumbnail Image

thumbnail
- {/* Tag */} + {/* Tags */}

Tag

- {detailData?.tags - ?.split(",") - .map((tag: string, i: number) => ( - 0 ? ( + tags.map((tag: string, i: number) => ( + {tag} - - ))} + + )) + ) : ( + — + )}
- {/* Notes */} + {/* Suggestion Box */}
-

Notes

-
- - +
+ + Suggestion Box (0) +
+ +
+

Description:

+ + {detailData?.isPublish ? ( + + Approved + + ) : ( + + Pending + + )} + +

Comment:

+

+ {detailData?.customCreatorName || "-"} +

+ +

setOpenHistory(true)} + > + View Approver History +

-
-
- -

Suggestion Box (0)

-
-
-

- Description : -

- {detailData?.isPublish === true ? ( - - Approved - - ) : ( - - Pending - - )} -

Comment

-

View Approver History

- {/* ================= ACTION BUTTON ================= */} + {/* ACTION */}
{isApproverOrAdmin(levelId) && !detailData?.isPublish && ( <> @@ -829,7 +859,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) { setApprovalStatus(4); doApproval(); }} - className="w-full bg-orange-500 hover:bg-orange-600 text-white py-3 rounded-xl" + className="w-full bg-orange-500 hover:bg-orange-600 text-white" > Revision @@ -839,17 +869,16 @@ export default function EditArticleForm(props: { isDetail: boolean }) { setApprovalStatus(5); doApproval(); }} - className="w-full bg-red-600 hover:bg-red-700 text-white py-3 rounded-xl" + className="w-full bg-red-600 hover:bg-red-700 text-white" > Reject )} - {/* 🔥 Jika levelId 3 → hanya tampilkan Cancel */} {isContributorRole(levelId) && ( - @@ -857,7 +886,230 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
+ + + + Approver History + + + {/* CONTENT */} +
+
+ {/* STEP 1 */} +
+ Upload +
+ + {/* LINE */} +
+ + {/* STEP 2 */} +
+

Level 1

+

Review oleh: Mabas Poin - Approver

+
+ + {/* LINE */} +
+ + {/* STEP 3 */} +
+ Publish +
+ + {/* NOTE */} +
+ Catatan: - +
+
+
+ + + + + + + +
); } + if (!isDetail) { + return ( +
+
+
+ {/* ================= LEFT ================= */} +
+ {/* TITLE */} +
+

Title

+ + {errors.title && ( +

+ {errors.title.message} +

+ )} +
+ + {/* SLUG */} +
+

Slug

+ +
+ + {/* CREATOR */} +
+

Creator

+ +
+ + {/* CATEGORY */} +
+

Category

+ ( + + )} + /> +
+ + {/* DESCRIPTION */} +
+

Description

+ setValue("description", val)} + /> +
+ + {/* MEDIA (UPLOAD) */} +
+

Media File

+ +
+ + +

+ Drag & drop atau klik untuk upload gambar +

+
+ + {/* PREVIEW FILE */} +
+ {files.map((file) => ( +
+ {file.name} + +
+ ))} +
+
+
+ + {/* ================= RIGHT ================= */} +
+
+ {/* THUMBNAIL */} +
+

Thumbnail

+ + {thumbnail ? ( + thumbnail + ) : ( +
+ No Image +
+ )} + + +
+ + {/* TAG */} +
+

Tags

+ + setTag(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + if (!tag) return; + setValue("tags", [...getValues("tags"), tag]); + setTag(""); + } + }} + /> + +
+ {getValues("tags")?.map((t, i) => ( + + {t} + { + const filtered = getValues("tags").filter( + (_, idx) => idx !== i, + ); + setValue("tags", filtered); + }} + /> + + ))} +
+
+
+ + {/* ACTION */} +
+ + + +
+
+
+
+
+ ); + } } diff --git a/components/landing-page/retracting-sidedar.tsx b/components/landing-page/retracting-sidedar.tsx index e14bb31..67b4994 100644 --- a/components/landing-page/retracting-sidedar.tsx +++ b/components/landing-page/retracting-sidedar.tsx @@ -64,7 +64,65 @@ const getSidebarByRole = (roleId: string | null) => { ]; } - if (roleId === "2" || roleId === "3") { + if (roleId === "2") { + return [ + { + title: "Dashboard", + items: [ + { + title: "Dashboard", + icon: () => ( + + ), + link: "/admin/dashboard", + }, + ], + }, + { + items: [ + { + title: "Content Website", + icon: () => , + link: "/admin/content-website", + }, + ], + }, + { + title: "News & Article", + items: [ + { + title: "News & Article", + icon: () => ( + + ), + children: [ + { + title: "Text", + icon: () => , + link: "/admin/news-article/text", + }, + { + title: "Image", + icon: () => , + link: "/admin/news-article/image", + }, + { + title: "Video", + icon: () => , + link: "/admin/news-article/video", + }, + { + title: "Audio", + icon: () => , + link: "/admin/news-article/audio", + }, + ], + }, + ], + }, + ]; + } + if (roleId === "3") { return [ { title: "Dashboard", diff --git a/components/main/news-article-list.tsx b/components/main/news-article-list.tsx index 0a4cd61..157cc2a 100644 --- a/components/main/news-article-list.tsx +++ b/components/main/news-article-list.tsx @@ -22,7 +22,10 @@ import Cookies from "js-cookie"; import { isContributorRole } from "@/constants/user-roles"; import Swal from "sweetalert2"; import type { ArticleContentKind } from "@/constants/article-content-types"; -import { ARTICLE_KIND_LABEL, articleListPath } from "@/constants/article-content-types"; +import { + ARTICLE_KIND_LABEL, + articleListPath, +} from "@/constants/article-content-types"; type Props = { kind: ArticleContentKind; @@ -36,6 +39,7 @@ export default function NewsArticleList({ kind, typeId }: Props) { const [search, setSearch] = useState(""); const [debouncedSearch, setDebouncedSearch] = useState(""); const [levelId, setLevelId] = useState(); + const [selectedTag, setSelectedTag] = useState("all"); useEffect(() => { setLevelId(Cookies.get("urie")); @@ -96,6 +100,7 @@ export default function NewsArticleList({ kind, typeId }: Props) { showCancelButton: true, }); if (!ok.isConfirmed) return; + loading(); try { const res = await deleteArticle(String(id)); @@ -108,12 +113,56 @@ export default function NewsArticleList({ kind, typeId }: Props) { return; } await fetchData(); - await Swal.fire({ icon: "success", title: "Deleted", timer: 1200, showConfirmButton: false }); + await Swal.fire({ + icon: "success", + title: "Deleted", + timer: 1200, + showConfirmButton: false, + }); } finally { close(); } } + const parseTags = (tags?: string) => { + if (!tags) return []; + return tags + .split(",") + .map((t) => t.replace("#", "").trim()) + .filter(Boolean); + }; + + const tagCounts: Record = {}; + + articles.forEach((article) => { + parseTags(article.tags).forEach((tag) => { + tagCounts[tag] = (tagCounts[tag] || 0) + 1; + }); + }); + + const tagEntries = Object.entries(tagCounts); + + const allOne = tagEntries.every(([, count]) => count === 1); + + let topTags: string[]; + + if (allOne) { + topTags = tagEntries + .sort(() => Math.random() - 0.5) + .slice(0, 5) + .map(([tag]) => tag); + } else { + topTags = tagEntries + .sort((a, b) => b[1] - a[1]) + .slice(0, 5) + .map(([tag]) => tag); + } + + const filteredArticles = + selectedTag === "all" + ? articles + : articles.filter((a) => parseTags(a.tags).includes(selectedTag)); + const label = ARTICLE_KIND_LABEL[kind]; const basePath = articleListPath(kind); @@ -122,12 +171,13 @@ export default function NewsArticleList({ kind, typeId }: Props) {

- News & Articles — {label} + News & Articles

- Create and manage {label.toLowerCase()} articles. Organize with tags only. + Create and manage {label.toLowerCase()} articles.

+ {isContributorRole(levelId) && (
+
+ + {topTags.map((tag) => ( + + ))} +
+ + {/* SEARCH */}
@@ -153,79 +230,93 @@ export default function NewsArticleList({ kind, typeId }: Props) {
+ {/* TABLE */} Article - Tags + Category + Author Status Date Actions + - {articles.length > 0 ? ( - articles.map((article) => ( - - - {article.title} - - - - {article.tags || "—"} - - - - {(() => { - const status = getStatus(article); - return ( - - {status} - - ); - })()} - - {formatDate(article.createdAt)} - - - - - - - - {isContributorRole(levelId) && ( - - )} - - - )) + {status} + + + + {formatDate(article.createdAt)} + + + + + + + + + + + {isContributorRole(levelId) && ( + + )} + + + ); + }) ) : ( - - No articles yet. Create one to get started. + + No articles found. )}
+ {/* PAGINATION */}

Page {page} of {totalPage} @@ -235,13 +326,15 @@ export default function NewsArticleList({ kind, typeId }: Props) { variant="outline" size="sm" disabled={page === 1} - onClick={() => setPage((p) => Math.max(1, p - 1))} + onClick={() => setPage((p) => p - 1)} > Previous + +