diff --git a/app/src/admin/ArticleList.jsx b/app/src/admin/ArticleList.jsx new file mode 100644 index 0000000..e17d4ca --- /dev/null +++ b/app/src/admin/ArticleList.jsx @@ -0,0 +1,165 @@ +import { useState, useEffect, useMemo } from 'react'; +import { useNav, useToast, CategoryBadge, StatusBadge, ConfirmModal, EmptyState, inputStyle } from '../../teknav-ui.jsx'; +import { adminApi } from '../lib/admin-api.js'; +import triangleAlertIcon from '../assets/icons/triangle-alert.svg'; +import { PanelState, actionBtn } from './Shared.jsx'; + +function ArticlesManagement({ role, onEdit }) { + const { navigate } = useNav(); + const toast = useToast(); + const [search, setSearch] = useState(''); + const [statusFilter, setStatusFilter] = useState(''); + const [freshnessFilter, setFreshnessFilter] = useState(''); + const [confirm, setConfirm] = useState(null); + const [articles, setArticles] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + const [showAll, setShowAll] = useState(false); + const PAGE_SIZE = 15; + + const refresh = () => { + setLoading(true); + setError(false); + return adminApi.listArticles({ limit: 100 }) + .then(res => setArticles(res.items ?? [])) + .catch(() => { setArticles([]); setError(true); }) + .finally(() => setLoading(false)); + }; + useEffect(() => { refresh(); }, []); + + useEffect(() => { + if (role !== 'admin' && role !== 'editor') return undefined; + const es = new EventSource('/api/v1/admin/stream', { withCredentials: true }); + es.addEventListener('view_update', event => { + const data = JSON.parse(event.data); + setArticles(list => list.map(article => article.id === data.articleId ? { ...article, views: data.views } : article)); + }); + return () => es.close(); + }, [role]); + + const filtered = useMemo(() => { + let list = articles; + if (role === 'reviewer') list = articles.filter(a => a.status === 'در انتظار بررسی'); + if (search) { const q = search.toLowerCase(); list = list.filter(a => a.title.toLowerCase().includes(q) || a.authorName?.toLowerCase().includes(q)); } + if (statusFilter) list = list.filter(a => a.status === statusFilter); + if (freshnessFilter) list = list.filter(a => a.contentFreshnessStatus === freshnessFilter); + return list; + }, [articles, search, statusFilter, freshnessFilter, role]); + + const changeStatus = async (id, newStatus) => { + try { + if (newStatus === 'منتشرشده') await adminApi.publishArticle(id); + else await adminApi.updateArticle(id, { status: newStatus }); + toast('وضعیت مقاله تغییر کرد'); + refresh(); + } catch (err) { + if (err?.body?.error === 'quality_check_failed') { + toast(`انتشار متوقف شد: ${err.body.issues?.join('، ')}`, 'error'); + } else { + toast('خطا در تغییر وضعیت مقاله', 'error'); + } + } + }; + + const deleteArt = async (id) => { + try { + await adminApi.deleteArticle(id); + toast('مقاله حذف شد'); + refresh(); + } catch { + toast('خطا در حذف مقاله', 'error'); + } + setConfirm(null); + }; + + const statusOptions = ['منتشرشده', 'پیش‌نویس', 'در انتظار بررسی', 'نیازمند اصلاح', 'زمان‌بندی‌شده']; + + return ( +
+ {confirm && deleteArt(confirm)} onCancel={() => setConfirm(null)} />} +
+ setSearch(e.target.value)} placeholder="جستجوی مقاله..." style={{ ...inputStyle, maxWidth: 260 }} dir="rtl" /> + + +
+
+
+ + + + {['عنوان', 'نویسنده', 'دسته', 'وضعیت', 'بازدید', 'تاریخ', 'اقدام'].map(h => ( + + ))} + + + + {(showAll ? filtered : filtered.slice(0, PAGE_SIZE)).map((a, i) => ( + + + + + + + + + + ))} + +
{h}
+
navigate('/article/' + a.slug)}>{a.title.slice(0, 50)}{a.title.length > 50 ? '…' : ''}
+
{a.readTime} دقیقه · {a.type}
+ {a.status === 'نیازمند اصلاح' && a.reviewNote && ( +
+
+ )} +
{a.authorName}{(a.views ?? 0).toLocaleString('fa-IR')}{a.date} +
+ {(role === 'admin' || role === 'editor') && a.status === 'در انتظار بررسی' && ( + <> + + + + )} + {role === 'writer' && a.status === 'پیش‌نویس' && ( + + )} + {role === 'reviewer' && ( + + )} + + {role === 'admin' && ( + + )} +
+
+
+ + {!loading && !error && articles.length > 0 && filtered.length === 0 && } + {!loading && !error && filtered.length > PAGE_SIZE && ( +
+ +
+ )} +
+
+ ); +} + +export { ArticlesManagement };