update UI
continuous-integration/drone/push Build is passing Details

This commit is contained in:
Anang Yusman 2026-04-15 15:09:53 +08:00
parent aeccdcffe9
commit eb039a3ca4
6 changed files with 570 additions and 145 deletions

View File

@ -0,0 +1,11 @@
import EditArticleForm from "@/components/form/article/edit-article-form";
export default function DetailArticlePage() {
return (
<div className="">
<div className="h-[96vh] p-3 lg:p-8 bg-slate-100 dark:!bg-black overflow-y-auto">
<EditArticleForm isDetail={false} />
</div>
</div>
);
}

View File

@ -0,0 +1,11 @@
import EditArticleForm from "@/components/form/article/edit-article-form";
export default function UpdateArticlePage() {
return (
<div className="">
<div className="h-[96vh] p-3 lg:p-8 bg-slate-100 dark:!bg-black overflow-y-auto">
<EditArticleForm isDetail={false} />
</div>
</div>
);
}

View File

@ -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;
}

View File

@ -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<Date | undefined>();
const [startTimeValue, setStartTimeValue] = useState<string>("");
const [openHistory, setOpenHistory] = useState(false);
const [levelId, setLevelId] = useState<string | undefined>();
@ -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<typeof createArticleSchema>;
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 (
<div className="min-h-screen bg-gray-100 p-3">
<div className="min-h-screen bg-gray-100 p-4">
{/* 🔹 BREADCRUMB
<div className="max-w-7xl mx-auto mb-4 text-sm text-gray-500 flex items-center gap-2">
<span className="text-blue-600 font-medium">News & Articles</span>
<span></span>
<span className="text-gray-700 font-medium">Detail Article</span>
</div> */}
<div className="max-w-7xl mx-auto flex flex-col lg:flex-row gap-6">
{/* ================= LEFT SIDE ================= */}
<div className="w-full lg:w-full bg-white rounded-2xl shadow-sm border p-4 space-y-3">
{/* ================= LEFT ================= */}
<div className="flex-1 bg-white rounded-2xl border shadow-sm p-6 space-y-6">
{/* Title */}
<div className="">
<p className="text-sm text-black mb-2">Title</p>
<div className="bg-gray-100 rounded-lg px-4 py-3 text-gray-800 text-lg font-medium">
<div>
<p className="text-sm text-gray-500 mb-2">Title</p>
<div className="bg-gray-100 rounded-lg px-4 py-3 text-gray-800 font-medium">
{detailData?.title}
</div>
</div>
{/* Content type (articles.type_id) */}
<div className="">
<p className="text-sm text-black mb-2">Content type</p>
{/* Category */}
<div>
<p className="text-sm text-gray-500 mb-2">Category</p>
<div className="bg-gray-100 rounded-lg px-4 py-3">
{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(", ")
: "—"}
</div>
</div>
{/* Description */}
<div className="">
<p className="text-sm text-black mb-4">Description</p>
<div className="prose max-w-none">
<p className="text-sm text-gray-500 mb-4">Description</p>
<ViewEditor initialData={detailData?.htmlDescription} />
</div>
</div>
{/* Media File */}
<div className="">
<p className="text-sm text-gray-500 mb-4">Media File</p>
{/* Media */}
<div>
<p className="text-sm text-gray-500 mb-3">Media File</p>
{detailfiles?.length > 0 ? (
{detailData?.files?.length > 0 ? (
<>
<Image
src={detailfiles[0]?.fileUrl}
src={detailData.files[0]?.fileUrl || ""}
width={900}
height={500}
alt="media"
className="rounded-xl w-full object-cover"
/>
<div className="flex gap-3 mt-4 overflow-x-auto">
{detailfiles.map((file: any, index: number) => (
<div className="flex gap-3 mt-3 overflow-x-auto">
{detailData.files.map((file: any, i: number) => (
<Image
key={index}
key={i}
src={file.fileUrl}
width={200}
height={120}
width={120}
height={80}
alt="preview"
className="rounded-lg h-24 w-40 object-cover border"
className="rounded-lg object-cover border"
/>
))}
</div>
</>
) : (
<p className="text-gray-400">Belum ada file</p>
<p className="text-gray-400">No media</p>
)}
</div>
</div>
{/* ================= RIGHT SIDE ================= */}
<div className="w-full lg:w-[30%] space-y-6">
{/* Meta & Thumbnail Card */}
<div className="bg-white rounded-2xl shadow-sm border p-6 space-y-6">
{/* ================= RIGHT ================= */}
<div className="w-full lg:w-[320px] space-y-6">
<div className="bg-white rounded-2xl border shadow-sm p-5 space-y-5">
{/* Creator */}
<div>
<p className="text-sm text-gray-500 mb-2">Creator</p>
<div className="bg-gray-100 rounded-lg px-4 py-3 font-medium">
{detailData?.createdByName || "-"}
</div>
</div>
{/* Thumbnail */}
<div>
<p className="text-sm text-gray-500 mb-2">Thumbnail Image</p>
<Image
src={detailData?.thumbnailUrl || "/default-avatar.png"}
src={detailData?.thumbnailUrl || "/placeholder.png"}
width={400}
height={250}
alt="thumbnail"
@ -767,59 +789,67 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
/>
</div>
{/* Tag */}
{/* Tags */}
<div>
<p className="text-sm text-gray-500 mb-2">Tag</p>
<div className="flex flex-wrap gap-2">
{detailData?.tags
?.split(",")
.map((tag: string, i: number) => (
<span
{tags.length > 0 ? (
tags.map((tag: string, i: number) => (
<Badge
key={i}
className="bg-gray-100 text-sm px-3 py-1 rounded-full border"
className="bg-gray-100 text-gray-700 rounded-full"
>
{tag}
</span>
))}
</Badge>
))
) : (
<span className="text-gray-400"></span>
)}
</div>
</div>
{/* Notes */}
{/* Suggestion Box */}
<div>
<p className="text-sm text-gray-500 mb-2">Notes</p>
<div className="bg-gray-100 rounded-lg px-4 py-3 text-gray-400">
-
<div className="flex items-center gap-2 text-blue-600 text-sm font-medium">
<Mail size={16} />
Suggestion Box (0)
</div>
</div>
<div className="flex items-center text-blue-700 gap-2">
<Mail size={20} />
<p className="text-sm ">Suggestion Box (0)</p>
</div>
<div className="border p-3 border-black rounded-lg space-y-2 ">
<h2 className="text-sm text-black font-semibold">
Description :
</h2>
{detailData?.isPublish === true ? (
<span className="inline-block bg-green-100 text-green-700 text-xs font-semibold px-3 py-1 rounded-full">
<div className="mt-3 border rounded-lg p-3 space-y-2">
<p className="text-sm font-semibold">Description:</p>
{detailData?.isPublish ? (
<span className="inline-block bg-green-100 text-green-700 text-xs px-3 py-1 rounded-full">
Approved
</span>
) : (
<span className="inline-block bg-yellow-100 text-yellow-700 text-xs font-semibold px-3 py-1 rounded-full">
<span className="inline-block bg-yellow-100 text-yellow-700 text-xs px-3 py-1 rounded-full">
Pending
</span>
)}
<p className="text-sm text-black font-semibold">Comment</p>
<h2 className="text-blue-600 text-xs">View Approver History</h2>
<p className="text-sm font-semibold">Comment:</p>
<p className="text-xs text-gray-500">
{detailData?.customCreatorName || "-"}
</p>
<p
className="text-xs text-blue-600 cursor-pointer"
onClick={() => setOpenHistory(true)}
>
View Approver History
</p>
</div>
</div>
</div>
{/* ================= ACTION BUTTON ================= */}
{/* ACTION */}
<div className="space-y-3">
{isApproverOrAdmin(levelId) && !detailData?.isPublish && (
<>
<Button
onClick={doPublish}
className="w-full bg-blue-600 hover:bg-blue-700 text-white py-3 rounded-xl"
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
>
Approve
</Button>
@ -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
</Button>
@ -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
</Button>
</>
)}
{/* 🔥 Jika levelId 3 → hanya tampilkan Cancel */}
{isContributorRole(levelId) && (
<Link href="/admin/news-article/image">
<Button variant="outline" className="w-full py-3 rounded-xl">
<Button variant="outline" className="w-full">
Cancel
</Button>
</Link>
@ -857,7 +886,230 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
</div>
</div>
</div>
<Dialog open={openHistory} onOpenChange={setOpenHistory}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Approver History</DialogTitle>
</DialogHeader>
{/* CONTENT */}
<div className="py-4">
<div className="flex flex-col items-center gap-4">
{/* STEP 1 */}
<div className="bg-blue-100 text-blue-700 px-4 py-2 rounded-full text-sm">
Upload
</div>
{/* LINE */}
<div className="w-[2px] h-10 bg-gray-300" />
{/* STEP 2 */}
<div className="bg-teal-500 text-white px-5 py-3 rounded-lg shadow">
<p className="text-sm font-semibold">Level 1</p>
<p className="text-xs">Review oleh: Mabas Poin - Approver</p>
</div>
{/* LINE */}
<div className="w-[2px] h-10 bg-gray-300" />
{/* STEP 3 */}
<div className="bg-green-500 text-white px-4 py-2 rounded-full text-sm">
Publish
</div>
{/* NOTE */}
<div className="bg-cyan-100 text-cyan-700 px-4 py-2 rounded-md text-sm mt-2 w-full text-center">
Catatan: -
</div>
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Tutup</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
if (!isDetail) {
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="min-h-screen bg-gray-100 p-4">
<div className="max-w-7xl mx-auto flex flex-col lg:flex-row gap-6">
{/* ================= LEFT ================= */}
<div className="flex-1 bg-white rounded-2xl border shadow-sm p-6 space-y-6">
{/* TITLE */}
<div>
<p className="text-sm text-gray-500 mb-2">Title</p>
<Input {...register("title")} />
{errors.title && (
<p className="text-red-500 text-xs mt-1">
{errors.title.message}
</p>
)}
</div>
{/* SLUG */}
<div>
<p className="text-sm text-gray-500 mb-2">Slug</p>
<Input {...register("slug")} />
</div>
{/* CREATOR */}
<div>
<p className="text-sm text-gray-500 mb-2">Creator</p>
<Input {...register("customCreatorName")} />
</div>
{/* CATEGORY */}
<div>
<p className="text-sm text-gray-500 mb-2">Category</p>
<Controller
name="category"
control={control}
render={({ field }) => (
<ReactSelect
{...field}
isMulti
options={listCategory}
components={animatedComponents}
/>
)}
/>
</div>
{/* DESCRIPTION */}
<div>
<p className="text-sm text-gray-500 mb-4">Description</p>
<CustomEditor
initialData={getValues("description")}
onChange={(val: string) => setValue("description", val)}
/>
</div>
{/* MEDIA (UPLOAD) */}
<div>
<p className="text-sm text-gray-500 mb-3">Media File</p>
<div
{...getRootProps()}
className="border-2 border-dashed rounded-xl p-6 text-center cursor-pointer hover:bg-gray-50"
>
<input {...getInputProps()} />
<CloudUploadIcon className="mx-auto mb-2" />
<p className="text-sm text-gray-500">
Drag & drop atau klik untuk upload gambar
</p>
</div>
{/* PREVIEW FILE */}
<div className="mt-4 space-y-2">
{files.map((file) => (
<div
key={file.name}
className="flex justify-between items-center border p-2 rounded-md"
>
<span className="text-sm">{file.name}</span>
<Button
size="icon"
variant="ghost"
onClick={() => handleRemoveFile(file)}
>
<TimesIcon />
</Button>
</div>
))}
</div>
</div>
</div>
{/* ================= RIGHT ================= */}
<div className="w-full lg:w-[320px] space-y-6">
<div className="bg-white rounded-2xl border shadow-sm p-5 space-y-5">
{/* THUMBNAIL */}
<div>
<p className="text-sm text-gray-500 mb-2">Thumbnail</p>
{thumbnail ? (
<Image
src={thumbnail}
width={400}
height={250}
alt="thumbnail"
className="rounded-xl w-full object-cover mb-2"
/>
) : (
<div className="w-full h-[200px] bg-gray-100 rounded-xl flex items-center justify-center text-gray-400">
No Image
</div>
)}
<Input type="file" onChange={handleFileChange} />
</div>
{/* TAG */}
<div>
<p className="text-sm text-gray-500 mb-2">Tags</p>
<Input
placeholder="Tekan enter untuk tambah tag"
value={tag}
onChange={(e) => setTag(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
if (!tag) return;
setValue("tags", [...getValues("tags"), tag]);
setTag("");
}
}}
/>
<div className="flex flex-wrap gap-2 mt-3">
{getValues("tags")?.map((t, i) => (
<Badge key={i} className="flex items-center gap-1">
{t}
<X
size={14}
className="cursor-pointer"
onClick={() => {
const filtered = getValues("tags").filter(
(_, idx) => idx !== i,
);
setValue("tags", filtered);
}}
/>
</Badge>
))}
</div>
</div>
</div>
{/* ACTION */}
<div className="space-y-3">
<Button
type="submit"
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
>
Save
</Button>
<Button
type="button"
variant="outline"
className="w-full"
onClick={() => router.back()}
>
Cancel
</Button>
</div>
</div>
</div>
</div>
</form>
);
}
}

View File

@ -64,7 +64,65 @@ const getSidebarByRole = (roleId: string | null) => {
];
}
if (roleId === "2" || roleId === "3") {
if (roleId === "2") {
return [
{
title: "Dashboard",
items: [
{
title: "Dashboard",
icon: () => (
<Icon icon="material-symbols:dashboard" className="text-lg" />
),
link: "/admin/dashboard",
},
],
},
{
items: [
{
title: "Content Website",
icon: () => <Icon icon="ri:article-line" className="text-lg" />,
link: "/admin/content-website",
},
],
},
{
title: "News & Article",
items: [
{
title: "News & Article",
icon: () => (
<Icon icon="grommet-icons:article" className="text-lg" />
),
children: [
{
title: "Text",
icon: () => <Icon icon="mdi:file-document-outline" />,
link: "/admin/news-article/text",
},
{
title: "Image",
icon: () => <Icon icon="mdi:image-outline" />,
link: "/admin/news-article/image",
},
{
title: "Video",
icon: () => <Icon icon="mdi:video-outline" />,
link: "/admin/news-article/video",
},
{
title: "Audio",
icon: () => <Icon icon="mdi:music-note-outline" />,
link: "/admin/news-article/audio",
},
],
},
],
},
];
}
if (roleId === "3") {
return [
{
title: "Dashboard",

View File

@ -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<string | undefined>();
const [selectedTag, setSelectedTag] = useState<string>("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<string, number> = {};
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) {
<div className="flex items-start justify-between gap-4">
<div>
<h1 className="text-2xl font-semibold text-slate-800">
News & Articles {label}
News & Articles
</h1>
<p className="text-sm text-slate-500 mt-1">
Create and manage {label.toLowerCase()} articles. Organize with tags only.
Create and manage {label.toLowerCase()} articles.
</p>
</div>
{isContributorRole(levelId) && (
<Link href={`${basePath}/create`}>
<Button className="bg-blue-600 hover:bg-blue-700 rounded-lg">
@ -138,6 +188,33 @@ export default function NewsArticleList({ kind, typeId }: Props) {
)}
</div>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setSelectedTag("all")}
className={`px-4 py-1.5 rounded-full text-sm text-black font-bold border ${
selectedTag === "all"
? "bg-blue-600 text-white"
: "bg-white text-slate-600"
}`}
>
All ({articles.length})
</button>
{topTags.map((tag) => (
<button
key={tag}
onClick={() => setSelectedTag(tag)}
className={`px-4 py-1.5 rounded-full text-sm text-black font-bold border ${
selectedTag === tag
? "bg-blue-600 text-white"
: "bg-white text-slate-600"
}`}
>
{tag} ({tagCounts[tag]})
</button>
))}
</div>
{/* SEARCH */}
<div className="flex gap-3">
<div className="relative flex-1">
<Search className="absolute left-3 top-3 w-4 h-4 text-slate-400" />
@ -153,34 +230,45 @@ export default function NewsArticleList({ kind, typeId }: Props) {
</div>
</div>
{/* TABLE */}
<Card className="rounded-2xl border shadow-sm">
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead className="pl-3">Article</TableHead>
<TableHead>Tags</TableHead>
<TableHead>Category</TableHead>
<TableHead>Author</TableHead>
<TableHead>Status</TableHead>
<TableHead>Date</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{articles.length > 0 ? (
articles.map((article) => (
{filteredArticles.length > 0 ? (
filteredArticles.map((article) => {
const status = getStatus(article);
return (
<TableRow key={article.id}>
<TableCell className="font-medium max-w-xs pl-3">
<TableCell className="font-medium pl-3">
{article.title}
</TableCell>
<TableCell>
<span className="text-sm text-slate-600 line-clamp-2">
{article.tags || "—"}
</span>
{article.categoryName ? (
<Badge className="bg-blue-100 text-blue-700 rounded-full">
{article.categoryName}
</Badge>
) : (
"—"
)}
</TableCell>
<TableCell>{article.createdByName || "—"}</TableCell>
<TableCell>
{(() => {
const status = getStatus(article);
return (
<span
className={`px-3 py-1 text-xs rounded-full font-medium ${statusClass(
status,
@ -188,26 +276,27 @@ export default function NewsArticleList({ kind, typeId }: Props) {
>
{status}
</span>
);
})()}
</TableCell>
<TableCell>{formatDate(article.createdAt)}</TableCell>
<TableCell className="text-right space-x-1">
<Link href={`/admin/news-article/detail/${article.id}`}>
<Button size="icon" variant="ghost" type="button">
<Button size="icon" variant="ghost">
<Eye className="w-4 h-4" />
</Button>
</Link>
<Link href={`/admin/news-article/detail/${article.id}`}>
<Button size="icon" variant="ghost" type="button">
<Link href={`/admin/news-article/update/${article.id}`}>
<Button size="icon" variant="ghost">
<Pencil className="w-4 h-4" />
</Button>
</Link>
{isContributorRole(levelId) && (
<Button
size="icon"
variant="ghost"
type="button"
onClick={() => handleDelete(article.id)}
>
<Trash2 className="w-4 h-4 text-red-500" />
@ -215,17 +304,19 @@ export default function NewsArticleList({ kind, typeId }: Props) {
)}
</TableCell>
</TableRow>
))
);
})
) : (
<TableRow>
<TableCell colSpan={5} className="text-center py-8 text-slate-500">
No articles yet. Create one to get started.
<TableCell colSpan={6} className="text-center py-8">
No articles found.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
{/* PAGINATION */}
<div className="flex items-center justify-between p-4 border-t text-sm text-slate-500">
<p>
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
</Button>
<Button size="sm" className="bg-blue-600">
{page}
</Button>
<Button
variant="outline"
size="sm"