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 className="flex flex-wrap gap-2">
|
||||
<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 variant="secondary" size="sm" asChild>
|
||||
<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