feat(blog-review): enhance BlogReviewPage with detailed article retrieval and loading state
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user