2026-02-17 10:02:35 +00:00
"use client" ;
import { Card , CardContent } from "@/components/ui/card" ;
import { Input } from "@/components/ui/input" ;
import { Button } from "@/components/ui/button" ;
import { Badge } from "@/components/ui/badge" ;
2026-04-13 17:47:40 +00:00
import { Search , Filter , ChevronLeft , ChevronRight } from "lucide-react" ;
import Link from "next/link" ;
import { useCallback , useEffect , useMemo , useState } from "react" ;
import Cookies from "js-cookie" ;
import { formatDate } from "@/utils/global" ;
import { listCmsContentSubmissions } from "@/service/cms-content-submissions" ;
import { getArticlesForMyContent } from "@/service/article" ;
import { apiPayload } from "@/service/cms-landing" ;
import { Loader2 } from "lucide-react" ;
const PLACEHOLDER_IMG =
"https://placehold.co/400x240/f1f5f9/64748b?text=Content" ;
type CmsRow = {
id : string ;
domain : string ;
title : string ;
status : string ;
submitter_name : string ;
submitted_by_id : number ;
created_at : string ;
} ;
2026-02-17 10:02:35 +00:00
2026-04-13 17:47:40 +00:00
type ArticleRow = {
id : number ;
title : string ;
thumbnailUrl? : string ;
isDraft? : boolean ;
isPublish? : boolean ;
publishStatus? : string ;
statusId? : number | null ;
createdAt? : string ;
created_at? : string ;
createdByName? : string ;
} ;
2026-02-17 10:02:35 +00:00
2026-04-13 17:47:40 +00:00
type UnifiedStatus = "draft" | "pending" | "approved" | "rejected" ;
2026-02-17 10:02:35 +00:00
2026-04-13 17:47:40 +00:00
type UnifiedItem = {
key : string ;
source : "website" | "news" ;
title : string ;
thumb : string ;
status : UnifiedStatus ;
statusLabel : string ;
date : string ;
href : string ;
} ;
2026-02-17 10:02:35 +00:00
2026-04-13 17:47:40 +00:00
function cmsToUnified ( r : CmsRow ) : UnifiedItem {
const st = ( r . status || "" ) . toLowerCase ( ) ;
let status : UnifiedStatus = "pending" ;
let statusLabel = "Pending Approval" ;
if ( st === "approved" ) {
status = "approved" ;
statusLabel = "Approved" ;
} else if ( st === "rejected" ) {
status = "rejected" ;
statusLabel = "Rejected" ;
} else if ( st === "pending" ) {
status = "pending" ;
statusLabel = "Pending Approval" ;
}
return {
key : ` cms- ${ r . id } ` ,
source : "website" ,
title : r.title ,
thumb : PLACEHOLDER_IMG ,
status ,
statusLabel ,
date : r.created_at ,
href : "/admin/content-website" ,
} ;
}
function articleHistoryStatus ( a : ArticleRow ) : UnifiedStatus {
if ( a . isDraft ) return "draft" ;
if ( a . isPublish ) return "approved" ;
const ps = ( a . publishStatus || "" ) . toLowerCase ( ) ;
if ( ps . includes ( "reject" ) ) return "rejected" ;
if ( a . statusId === 3 ) return "rejected" ;
return "pending" ;
}
function articleStatusLabel ( s : UnifiedStatus ) : string {
switch ( s ) {
case "draft" :
return "Draft" ;
case "pending" :
return "Pending Approval" ;
case "approved" :
return "Approved" ;
case "rejected" :
return "Rejected" ;
2026-02-17 10:02:35 +00:00
default :
2026-04-13 17:47:40 +00:00
return "Pending Approval" ;
2026-02-17 10:02:35 +00:00
}
2026-04-13 17:47:40 +00:00
}
function articleToUnified ( r : ArticleRow ) : UnifiedItem {
const status = articleHistoryStatus ( r ) ;
const rawDate = r . createdAt ? ? r . created_at ? ? "" ;
return {
key : ` art- ${ r . id } ` ,
source : "news" ,
title : r.title || "Untitled" ,
thumb : r.thumbnailUrl?.trim ( ) || PLACEHOLDER_IMG ,
status ,
statusLabel : articleStatusLabel ( status ) ,
date : rawDate ,
href : ` /admin/news-article/detail/ ${ r . id } ` ,
} ;
}
function statusBadgeClass ( status : UnifiedStatus ) : string {
switch ( status ) {
case "pending" :
return "bg-yellow-100 text-yellow-800 border-yellow-200" ;
case "approved" :
return "bg-green-100 text-green-800 border-green-200" ;
case "rejected" :
return "bg-red-100 text-red-800 border-red-200" ;
case "draft" :
default :
return "bg-slate-100 text-slate-700 border-slate-200" ;
}
}
const PAGE_SIZE = 8 ;
2026-02-17 10:02:35 +00:00
export default function MyContent() {
2026-04-13 17:47:40 +00:00
const [ levelId , setLevelId ] = useState < string | undefined > ( ) ;
2026-02-17 10:02:35 +00:00
const [ search , setSearch ] = useState ( "" ) ;
2026-04-13 17:47:40 +00:00
const [ debouncedSearch , setDebouncedSearch ] = useState ( "" ) ;
const [ sourceFilter , setSourceFilter ] = useState < "all" | "news" | "website" > (
"all" ,
) ;
const [ statusFilter , setStatusFilter ] = useState <
"all" | UnifiedStatus
> ( "all" ) ;
const [ loading , setLoading ] = useState ( true ) ;
const [ cmsRows , setCmsRows ] = useState < CmsRow [ ] > ( [ ] ) ;
const [ articleRows , setArticleRows ] = useState < ArticleRow [ ] > ( [ ] ) ;
const [ page , setPage ] = useState ( 1 ) ;
const isApprover = levelId === "3" ;
const isContributor = levelId === "2" ;
useEffect ( ( ) = > {
setLevelId ( Cookies . get ( "ulne" ) ) ;
} , [ ] ) ;
useEffect ( ( ) = > {
const t = setTimeout ( ( ) = > setDebouncedSearch ( search ) , 350 ) ;
return ( ) = > clearTimeout ( t ) ;
} , [ search ] ) ;
const load = useCallback ( async ( ) = > {
if ( levelId === undefined ) return ;
setLoading ( true ) ;
try {
const cmsMine = isContributor ;
const cmsRes = await listCmsContentSubmissions ( {
status : "all" ,
mine : cmsMine ,
page : 1 ,
limit : 500 ,
} ) ;
const cmsData = apiPayload < CmsRow [ ] > ( cmsRes ) ;
setCmsRows ( Array . isArray ( cmsData ) ? cmsData : [ ] ) ;
const artMode = isApprover ? "approver" : "own" ;
const artRes = await getArticlesForMyContent ( {
mode : artMode ,
page : 1 ,
limit : 500 ,
} ) ;
const artData = apiPayload < ArticleRow [ ] > ( artRes ) ;
setArticleRows ( Array . isArray ( artData ) ? artData : [ ] ) ;
} finally {
setLoading ( false ) ;
}
} , [ levelId , isContributor , isApprover ] ) ;
useEffect ( ( ) = > {
load ( ) ;
} , [ load ] ) ;
const mergedAll = useMemo ( ( ) = > {
const cms = cmsRows . map ( cmsToUnified ) ;
const arts = articleRows . map ( articleToUnified ) ;
let list = [ . . . cms , . . . arts ] ;
const q = search . trim ( ) . toLowerCase ( ) ;
if ( q ) {
list = list . filter (
( x ) = >
x . title . toLowerCase ( ) . includes ( q ) ||
x . statusLabel . toLowerCase ( ) . includes ( q ) ,
) ;
}
if ( sourceFilter === "news" ) list = list . filter ( ( x ) = > x . source === "news" ) ;
if ( sourceFilter === "website" )
list = list . filter ( ( x ) = > x . source === "website" ) ;
if ( statusFilter !== "all" )
list = list . filter ( ( x ) = > x . status === statusFilter ) ;
list . sort ( ( a , b ) = > ( a . date < b . date ? 1 : - 1 ) ) ;
return list ;
} , [ cmsRows , articleRows , debouncedSearch , sourceFilter , statusFilter ] ) ;
const stats = useMemo ( ( ) = > {
const draft = mergedAll . filter ( ( x ) = > x . status === "draft" ) . length ;
const pending = mergedAll . filter ( ( x ) = > x . status === "pending" ) . length ;
const approved = mergedAll . filter ( ( x ) = > x . status === "approved" ) . length ;
const rejected = mergedAll . filter ( ( x ) = > x . status === "rejected" ) . length ;
return {
total : mergedAll.length ,
draft ,
pending ,
approved ,
rejected ,
} ;
} , [ mergedAll ] ) ;
const totalPages = Math . max ( 1 , Math . ceil ( mergedAll . length / PAGE_SIZE ) ) ;
const currentPage = Math . min ( page , totalPages ) ;
const pageItems = useMemo ( ( ) = > {
const start = ( currentPage - 1 ) * PAGE_SIZE ;
return mergedAll . slice ( start , start + PAGE_SIZE ) ;
} , [ mergedAll , currentPage ] ) ;
useEffect ( ( ) = > {
setPage ( 1 ) ;
} , [ sourceFilter , statusFilter , debouncedSearch , levelId ] ) ;
if ( levelId === undefined ) {
return (
< div className = "flex min-h-[200px] items-center justify-center" >
< Loader2 className = "h-8 w-8 animate-spin text-slate-400" / >
< / div >
) ;
}
2026-02-17 10:02:35 +00:00
return (
< div className = "space-y-8" >
< div >
2026-04-13 17:47:40 +00:00
< h1 className = "text-2xl font-semibold text-slate-900" > My Content < / h1 >
< p className = "text-sm text-muted-foreground mt-1" >
Track all your content submissions and drafts .
< / p >
< p className = "text-xs text-slate-500 mt-2 max-w-3xl" >
{ isContributor
? "Riwayat konten Anda: perubahan Content Website (setelah diajukan) dan artikel. Buka item untuk mengedit atau melanjutkan persetujuan di halaman masing-masing."
: isApprover
? "Riwayat dari kontributor: Content Website dan News & Articles (tanpa draft). Persetujuan dilakukan di halaman Content Website atau detail artikel."
: "Ringkasan konten terkait akun Anda." }
2026-02-17 10:02:35 +00:00
< / p >
< / div >
< div className = "grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4" >
2026-04-13 17:47:40 +00:00
{ [
{ title : "Total Content" , value : stats.total , color : "bg-blue-500" } ,
{ title : "Drafts" , value : stats.draft , color : "bg-slate-600" } ,
{ title : "Pending" , value : stats.pending , color : "bg-yellow-500" } ,
{ title : "Approved" , value : stats.approved , color : "bg-green-600" } ,
{
title : "Revision/Rejected" ,
value : stats.rejected ,
color : "bg-red-600" ,
} ,
] . map ( ( item ) = > (
< Card key = { item . title } className = "rounded-2xl shadow-sm" >
2026-02-17 10:02:35 +00:00
< CardContent className = "p-5 flex items-center gap-4" >
< div
2026-04-13 17:47:40 +00:00
className = { ` w-12 h-12 rounded-xl flex items-center justify-center text-white shrink-0 ${ item . color } ` }
/ >
2026-02-17 10:02:35 +00:00
< div >
2026-04-13 17:47:40 +00:00
< p className = "text-2xl font-bold text-slate-900" > { item . value } < / p >
2026-02-17 10:02:35 +00:00
< p className = "text-sm text-muted-foreground" > { item . title } < / p >
< / div >
< / CardContent >
< / Card >
) ) }
< / div >
< div className = "flex flex-col md:flex-row gap-3 md:items-center md:justify-between" >
< div className = "relative w-full md:max-w-md" >
< Search className = "absolute left-3 top-3 w-4 h-4 text-muted-foreground" / >
< Input
2026-04-13 17:47:40 +00:00
placeholder = "Search by title…"
2026-02-17 10:02:35 +00:00
className = "pl-9"
value = { search }
onChange = { ( e ) = > setSearch ( e . target . value ) }
/ >
< / div >
2026-04-13 17:47:40 +00:00
< div className = "flex flex-wrap gap-2 items-center" >
< div className = "flex items-center gap-2 rounded-lg border bg-white px-3 py-2" >
< Filter className = "w-4 h-4 text-muted-foreground" / >
< select
className = "text-sm bg-transparent border-none outline-none"
value = { sourceFilter }
onChange = { ( e ) = >
setSourceFilter ( e . target . value as typeof sourceFilter )
}
>
< option value = "all" > All sources < / option >
< option value = "news" > News & Articles < / option >
< option value = "website" > Content Website < / option >
< / select >
< / div >
< div className = "flex items-center gap-2 rounded-lg border bg-white px-3 py-2" >
< select
className = "text-sm bg-transparent border-none outline-none"
value = { statusFilter }
onChange = { ( e ) = >
setStatusFilter ( e . target . value as typeof statusFilter )
}
>
< option value = "all" > All statuses < / option >
< option value = "draft" > Draft < / option >
< option value = "pending" > Pending Approval < / option >
< option value = "approved" > Approved < / option >
< option value = "rejected" > Rejected < / option >
< / select >
< / div >
< Button
type = "button"
variant = "outline"
size = "sm"
onClick = { ( ) = > load ( ) }
disabled = { loading }
>
Refresh
< / Button >
< / div >
2026-02-17 10:02:35 +00:00
< / div >
2026-04-13 17:47:40 +00:00
{ loading ? (
< div className = "flex justify-center py-20" >
< Loader2 className = "h-10 w-10 animate-spin text-[#966314]" / >
< / div >
) : pageItems . length === 0 ? (
< p className = "text-center text-slate-500 py-16" > No content found . < / p >
) : (
< >
< div className = "grid sm:grid-cols-2 lg:grid-cols-4 gap-6" >
{ pageItems . map ( ( item ) = > (
< Link key = { item . key } href = { item . href } className = "block group" >
< Card className = "rounded-2xl border border-slate-200 shadow-sm hover:shadow-md transition-all duration-200 bg-white h-full overflow-hidden" >
< div className = "flex items-center gap-1 flex-wrap px-3 pt-3" >
< Badge
className = { ` ${ statusBadgeClass ( item . status ) } border font-medium ` }
>
{ item . statusLabel }
< / Badge >
< Badge
variant = "outline"
className = "text-blue-600 border-blue-200 bg-blue-50"
>
{ item . source === "news"
? "News & Articles"
: "Content Website" }
< / Badge >
< / div >
< div className = "relative w-full aspect-[4/3] mt-2 bg-slate-100" >
{ /* eslint-disable-next-line @next/next/no-img-element */ }
< img
src = { item . thumb }
alt = ""
className = "absolute inset-0 w-full h-full object-cover"
/ >
< / div >
< CardContent className = "px-4 pb-4 pt-3" >
< h3 className = "text-sm font-semibold leading-snug line-clamp-2 text-slate-900 group-hover:text-blue-700" >
{ item . title }
< / h3 >
< p className = "text-xs text-slate-500 mt-2" >
{ item . date ? formatDate ( item . date ) : "—" }
< / p >
< / CardContent >
< / Card >
< / Link >
) ) }
< / div >
2026-02-17 10:02:35 +00:00
2026-04-13 17:47:40 +00:00
< div className = "flex flex-col sm:flex-row justify-center items-center gap-3 pt-6" >
< p className = "text-sm text-slate-500" >
Showing { ( currentPage - 1 ) * PAGE_SIZE + 1 } to { " " }
{ Math . min ( currentPage * PAGE_SIZE , mergedAll . length ) } of { " " }
{ mergedAll . length } items
< / p >
< div className = "flex items-center gap-2" >
< Button
type = "button"
2026-02-17 10:02:35 +00:00
variant = "outline"
2026-04-13 17:47:40 +00:00
size = "sm"
disabled = { currentPage <= 1 }
onClick = { ( ) = > setPage ( ( p ) = > Math . max ( 1 , p - 1 ) ) }
2026-02-17 10:02:35 +00:00
>
2026-04-13 17:47:40 +00:00
< ChevronLeft className = "w-4 h-4" / >
Previous
< / Button >
< span className = "text-sm text-slate-600 px-2" >
Page { currentPage } / { totalPages }
< / span >
< Button
type = "button"
variant = "outline"
size = "sm"
disabled = { currentPage >= totalPages }
onClick = { ( ) = > setPage ( ( p ) = > Math . min ( totalPages , p + 1 ) ) }
>
Next
< ChevronRight className = "w-4 h-4" / >
< / Button >
2026-02-17 10:02:35 +00:00
< / div >
2026-04-13 17:47:40 +00:00
< / div >
< / >
) }
2026-02-17 10:02:35 +00:00
< / div >
) ;
}