feat(blog-review): enhance BlogReviewPage with detailed article retrieval and loading state

This commit is contained in:
hamed
2026-08-04 20:38:09 +03:30
parent 9d767a6c63
commit f9bbdf1e7c
3 changed files with 187 additions and 6 deletions
+70 -6
View File
@@ -4,7 +4,7 @@ import { toast } from 'sonner';
import { api } from '../lib/api';
import type { ApiResponse, PaginatedResponse } from '../lib/api';
import type { Blog } from '../types';
import { formatDate } from '../lib/utils';
import { formatDate, formatNumber } from '../lib/utils';
import PageHeader from '../components/ui/PageHeader';
import DataTable, { Column } from '../components/ui/DataTable';
import Pagination from '../components/ui/Pagination';
@@ -50,6 +50,22 @@ export default function BlogReviewPage() {
const items = data?.data ?? [];
const total = data?.meta?.totalRecords ?? 0;
/**
* ردیف‌های صف از `toListArray()` می‌آیند و `body`/`faq`/`sources` ندارند — مودال
* بازبینی با آن‌ها یک قاب خالی بود. متن کامل هنگام باز شدن مودال از اندپوینت
* جزئیاتِ موجود گرفته می‌شود، نه با سنگین‌کردن پاسخ فهرست برای ۱۵ ردیف.
*
* شکل پاسخ عمداً double-nested است (`data.data`) — همان قراردادی که فرم ویرایش
* مقاله هم مصرف می‌کند.
*/
const { data: detail, isLoading: detailLoading } = useQuery({
queryKey: ['admin-blog', preview?.uuid],
queryFn: () => api.get<ApiResponse<{ data: Blog }>>(`/api/v1/admin/blog/${preview!.uuid}`),
enabled: !!preview,
});
const full = detail?.data?.data ?? null;
const approveMutation = useMutation({
mutationFn: (blog: Blog) =>
api.post<ApiResponse<Blog>>(`/api/v1/admin/blog/${blog.uuid}/review`, {
@@ -153,19 +169,67 @@ export default function BlogReviewPage() {
) : undefined
}
>
{preview && (
{preview && detailLoading && !full && (
<p className="muted">در حال بارگذاری مقاله...</p>
)}
{full && (
<div className="blog-review-preview">
{preview.summary && <p className="muted">{preview.summary}</p>}
{full.image_url && (
<img
src={full.image_url}
alt=""
style={{
width: '100%', maxHeight: 320, objectFit: 'cover',
borderRadius: 'var(--r)', marginBottom: 'var(--gap)',
}}
/>
)}
{/* شناسنامهٔ مقاله: بازبین بدون این‌ها نمی‌داند چه چیزی را تأیید می‌کند */}
<div
className="muted"
style={{ display: 'flex', gap: 10, flexWrap: 'wrap', fontSize: 12.5, marginBottom: 10 }}
>
{full.topic_slug && <span className="mono">{full.topic_slug}</span>}
{full.reading_time ? <span>{formatNumber(full.reading_time)} دقیقه مطالعه</span> : null}
{full.primary_keyword && <span>کلیدواژه: {full.primary_keyword}</span>}
<span>{formatDate(full.created_at)}</span>
</div>
{full.summary && <p className="muted">{full.summary}</p>}
<div
className="blog-body"
/* محتوای تولیدشده و بازبینی‌شده توسط ادمین است */
dangerouslySetInnerHTML={{ __html: preview.body }}
dangerouslySetInnerHTML={{ __html: full.body }}
/>
{preview.sources && preview.sources.length > 0 && (
{full.faq && full.faq.length > 0 && (
<div style={{ marginTop: 'var(--gap)' }}>
<h4 className="section-title">پرسشهای متداول</h4>
{full.faq.map((f, i) => (
<div key={i} style={{ marginBottom: 10 }}>
<div style={{ fontWeight: 600 }}>{f.q}</div>
<div className="muted">{f.a}</div>
</div>
))}
</div>
)}
{full.tags && full.tags.length > 0 && (
<div style={{ marginTop: 'var(--gap)', display: 'flex', gap: 6, flexWrap: 'wrap' }}>
{full.tags.map((t) => (
<span key={t} className="badge gray">{t}</span>
))}
</div>
)}
{full.sources && full.sources.length > 0 && (
<div className="blog-sources" style={{ marginTop: 'var(--gap)' }}>
<h4 className="section-title">منابع</h4>
<ul>
{preview.sources.map((s) => (
{full.sources.map((s) => (
<li key={s.url}>
<a href={s.url} target="_blank" rel="noopener noreferrer">
{s.title || s.url}