fix(frontend): add blog admin preview route
Some checks failed
Frontend CI/CD / build (push) Has been cancelled
Frontend CI/CD / deploy (push) Has been cancelled

This commit is contained in:
2026-06-08 22:05:48 +03:30
parent 49dcb1dd1b
commit 37b123838f
3 changed files with 181 additions and 1 deletions

View 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)} />;
}

View File

@@ -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>

View 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>
);
}