fix(frontend): add blog admin preview route
This commit is contained in:
8
src/app/admin/blog/[id]/preview/page.tsx
Normal file
8
src/app/admin/blog/[id]/preview/page.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import AdminBlogPreview from "@/views/AdminBlogPreview";
|
||||||
|
|
||||||
|
type Params = Promise<{ id: string }>;
|
||||||
|
|
||||||
|
export default async function AdminBlogPreviewPage({ params }: { params: Params }) {
|
||||||
|
const { id } = await params;
|
||||||
|
return <AdminBlogPreview postId={Number(id)} />;
|
||||||
|
}
|
||||||
@@ -146,7 +146,7 @@ export default function AdminBlog() {
|
|||||||
<div key={post.id} className="flex flex-col gap-3 rounded-2xl border p-4 md:flex-row md:items-center md:justify-between">
|
<div key={post.id} className="flex flex-col gap-3 rounded-2xl border p-4 md:flex-row md:items-center md:justify-between">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<Button variant="outline" size="sm" asChild>
|
<Button variant="outline" size="sm" asChild>
|
||||||
<Link to={`/blog/${post.slug}`}><Eye className="ml-2 h-4 w-4" />مشاهده</Link>
|
<Link to={`/admin/blog/${post.id}/preview`}><Eye className="ml-2 h-4 w-4" />پیشنمایش</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="secondary" size="sm" asChild>
|
<Button variant="secondary" size="sm" asChild>
|
||||||
<Link to={`/admin/blog/${post.id}/edit`}><Edit className="ml-2 h-4 w-4" />ویرایش</Link>
|
<Link to={`/admin/blog/${post.id}/edit`}><Edit className="ml-2 h-4 w-4" />ویرایش</Link>
|
||||||
|
|||||||
172
src/views/AdminBlogPreview.tsx
Normal file
172
src/views/AdminBlogPreview.tsx
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { ArrowRight, Edit, ExternalLink, Loader2 } from "lucide-react";
|
||||||
|
import Markdown from "@/components/Markdown";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { api } from "@/lib/api";
|
||||||
|
import { Link } from "@/lib/router";
|
||||||
|
import { apiBaseUrl, toAbsoluteUrl } from "@/lib/site";
|
||||||
|
import type * as Types from "@/lib/types";
|
||||||
|
import { formatJalali, resolveErrorMessage } from "@/lib/utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
postId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusLabels: Record<string, string> = {
|
||||||
|
draft: "پیشنویس",
|
||||||
|
submitted: "در انتظار بررسی",
|
||||||
|
changes_requested: "نیازمند اصلاح",
|
||||||
|
published: "منتشر شده",
|
||||||
|
archived: "آرشیو شده",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AdminBlogPreview({ postId }: Props) {
|
||||||
|
const [post, setPost] = useState<Types.PostDetailSchema | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!Number.isFinite(postId)) {
|
||||||
|
setError("شناسه نوشته نامعتبر است.");
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isMounted = true;
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
api.getAdminBlogPost(postId)
|
||||||
|
.then((data) => {
|
||||||
|
if (isMounted) setPost(data);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (isMounted) {
|
||||||
|
setError(resolveErrorMessage(err, "نوشته برای پیشنمایش یافت نشد."));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (isMounted) setLoading(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
|
}, [postId]);
|
||||||
|
|
||||||
|
const image = useMemo(() => {
|
||||||
|
if (!post) return undefined;
|
||||||
|
return toAbsoluteUrl(
|
||||||
|
post.og_image_url || post.absolute_featured_image_url || post.featured_image,
|
||||||
|
apiBaseUrl,
|
||||||
|
);
|
||||||
|
}, [post]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-[50vh] items-center justify-center" dir="rtl">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin text-primary" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !post) {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-8" dir="rtl">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="text-right">
|
||||||
|
<CardTitle>پیشنمایش نوشته در دسترس نیست</CardTitle>
|
||||||
|
<CardDescription>{error}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Button asChild variant="outline">
|
||||||
|
<Link to="/admin/blog">
|
||||||
|
<ArrowRight className="ml-2 h-4 w-4" />
|
||||||
|
بازگشت به مدیریت بلاگ
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background" dir="rtl">
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<div className="mb-6 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Button asChild variant="outline">
|
||||||
|
<Link to="/admin/blog">
|
||||||
|
<ArrowRight className="ml-2 h-4 w-4" />
|
||||||
|
بازگشت
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button asChild variant="secondary">
|
||||||
|
<Link to={`/admin/blog/${post.id}/edit`}>
|
||||||
|
<Edit className="ml-2 h-4 w-4" />
|
||||||
|
ویرایش
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
{post.status === "published" && post.slug ? (
|
||||||
|
<Button asChild>
|
||||||
|
<Link to={`/blog/${post.slug}`}>
|
||||||
|
<ExternalLink className="ml-2 h-4 w-4" />
|
||||||
|
نسخه عمومی
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<Badge variant={post.status === "published" ? "default" : "secondary"}>
|
||||||
|
{statusLabels[post.status] ?? post.status}
|
||||||
|
</Badge>
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
آخرین بهروزرسانی: {formatJalali(post.updated_at, false)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{post.status !== "published" ? (
|
||||||
|
<div className="mb-4 rounded-2xl border border-amber-200 bg-amber-50 p-4 text-right text-sm text-amber-900 dark:border-amber-900/50 dark:bg-amber-950/30 dark:text-amber-100">
|
||||||
|
این نسخه پیشنمایش داخلی است و تا زمان انتشار در صفحه عمومی وبلاگ دیده نمیشود.
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
{image ? (
|
||||||
|
<div className="aspect-video w-full overflow-hidden rounded-t-lg bg-muted">
|
||||||
|
<img src={image} alt={post.title} className="h-full w-full object-cover" />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{post.category?.name ? <Badge variant="secondary">{post.category.name}</Badge> : null}
|
||||||
|
{post.tags.map((tag) => (
|
||||||
|
<Badge key={tag.id} variant="outline">
|
||||||
|
{tag.name}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<CardTitle className="text-3xl leading-relaxed">{post.title}</CardTitle>
|
||||||
|
<CardDescription className="text-sm">
|
||||||
|
نویسنده: {[post.author.first_name, post.author.last_name].filter(Boolean).join(" ") || post.author.username}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
{post.excerpt ? (
|
||||||
|
<p className="rounded-lg border bg-muted/30 p-4 text-sm leading-7 text-muted-foreground">
|
||||||
|
{post.excerpt}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
<Markdown content={post.content} justify size="base" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user