admin/app/(dashboard)/blog-articles/page.tsx
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

147 lines
5.0 KiB
TypeScript

'use client'
import { useRef, useState } from 'react'
import dynamic from 'next/dynamic'
import type { PaginationListColumnType } from '@/types'
import { addToast } from '@/lib/toast'
import Button from '@/components/formElements/Button'
import PaginatedList, { type PaginatedListHandle } from '@/components/PaginatedList'
import PageNavbar from '@/components/layouts/PageNavbar'
import StatusChip from '@/components/ui/StatusChip'
import EditIconOutline from '@/components/icons/EditIconOutline'
import TrashIcon from '@/components/icons/TrashIcon'
import useAlertModal from '@/hooks/useAlertModal'
import { getFeaturedStatus, getPublishStatus } from '@/constants/status'
import { formatPersianDate } from '@/lib/formatters'
import { API_ROUTES } from '@/services/config'
import { type BlogArticle, DELETE_ARTICLE, GET_ARTICLE } from '@/services/blogArticles'
const ArticleFormModal = dynamic(() => import('@/app/(dashboard)/blog-articles/_components/ArticleFormModal'), { ssr: false })
interface ArticleRow {
id: number
title: string
slug: string
categoryName: string
isPublished: boolean
isFeatured: boolean
scheduledAt: string | null
updatedAt: string
[key: string]: unknown
}
const columns: PaginationListColumnType[] = [
{ field: 'title', label: 'عنوان', filterable: true, sortable: true, type: 'text' },
{ field: 'categoryName', label: 'دسته‌بندی', filterable: false, sortable: false },
{ field: 'isPublished', label: 'وضعیت', filterable: false, sortable: false },
{ field: 'isFeatured', label: 'ویژه', filterable: false, sortable: false },
{ field: 'updatedAt', label: 'آخرین به‌روزرسانی', filterable: false, sortable: true },
{ field: 'actions', label: 'عملیات' },
]
const BlogArticlesAdminPage = () => {
const listRef = useRef<PaginatedListHandle>(null)
const { showAlert } = useAlertModal()
const [modalState, setModalState] = useState<{ article?: BlogArticle } | null>(null)
const openCreate = () => {
setModalState({})
}
const openEdit = async (id: number) => {
const result = await GET_ARTICLE(id)
if (!result.ok) return
setModalState({ article: result.data })
}
const handleDelete = (row: ArticleRow) => {
showAlert(`مقاله «${row.title}» حذف شود؟`, async () => {
const result = await DELETE_ARTICLE(row.id)
if (!result.ok) return
addToast({ title: 'مقاله حذف شد', color: 'success' })
listRef.current?.refresh()
})
}
return (
<section className="h-full w-full text-right">
<PageNavbar pageTitle="مقالات وبلاگ" />
<div className="admin-page-container space-y-4">
<PaginatedList
ref={listRef}
hasDynamicButton
columns={columns}
dynamicButtonText="مقاله جدید"
url={API_ROUTES.BLOG_ARTICLES.ADMIN_LIST}
urlParams={{ page: 1, pageSize: 20, sort: '-updatedAt', filters: {} }}
onDynamicButtonClick={openCreate}
>
{{
isPublished: (row) => {
const article = row as ArticleRow
return (
<StatusChip
{...getPublishStatus(article.isPublished, article.scheduledAt)}
description={article.scheduledAt ? formatPersianDate(article.scheduledAt) : undefined}
/>
)
},
isFeatured: (row) => {
const featured = getFeaturedStatus((row as ArticleRow).isFeatured)
return featured ? <StatusChip {...featured} /> : '—'
},
updatedAt: (_row, cellValue) => formatPersianDate(cellValue),
actions: (row) => {
const article = row as ArticleRow
return (
<div className="flex items-center gap-1">
<Button
iconOnly
aria-label="ویرایش مقاله"
size="sm"
variant="flat"
onClick={() => openEdit(article.id)}
>
<EditIconOutline className="size-4" />
</Button>
<Button
iconOnly
aria-label="حذف مقاله"
color="danger"
size="sm"
variant="flat"
onClick={() => {
handleDelete(article)
}}
>
<TrashIcon className="size-4 text-fourth-900" />
</Button>
</div>
)
},
}}
</PaginatedList>
</div>
{modalState ? (
<ArticleFormModal
article={modalState.article}
isOpen={modalState !== null}
onOpenChange={(open) => {
if (!open) setModalState(null)
}}
onSuccess={() => listRef.current?.refresh()}
/>
) : null}
</section>
)
}
export default BlogArticlesAdminPage