Files
guilan-ace-frontend/src/views/AdminBlogAssets.tsx

188 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useRef, useState } from "react";
import { ArrowRight, Copy, Loader2, Trash2, UploadCloud } from "lucide-react";
import { useRouter } from "next/navigation";
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 type * as Types from "@/lib/types";
import { resolveErrorMessage } from "@/lib/utils";
import { useToast } from "@/hooks/use-toast";
type Props = {
postId: number;
};
export default function AdminBlogAssets({ postId }: Props) {
const router = useRouter();
const { toast } = useToast();
const fileInputRef = useRef<HTMLInputElement | null>(null);
const [post, setPost] = useState<Types.PostDetailSchema | null>(null);
const [assets, setAssets] = useState<Types.PostAssetSchema[]>([]);
const [loading, setLoading] = useState(true);
const [uploading, setUploading] = useState(false);
const [deletingId, setDeletingId] = useState<number | null>(null);
const loadData = async () => {
if (!Number.isFinite(postId)) {
setLoading(false);
return;
}
setLoading(true);
try {
const [postData, assetData] = await Promise.all([
api.getAdminBlogPost(postId),
api.listBlogPostAssets(postId),
]);
setPost(postData);
setAssets(assetData);
} catch (error) {
toast({
title: "دریافت مرکز آپلود ناموفق بود",
description: resolveErrorMessage(error, "دوباره تلاش کنید"),
variant: "destructive",
});
} finally {
setLoading(false);
}
};
useEffect(() => {
void loadData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [postId]);
const uploadAsset = async (file: File) => {
setUploading(true);
try {
const asset = await api.uploadBlogPostAsset(postId, file, { title: file.name });
setAssets((prev) => [asset, ...prev]);
toast({ title: "فایل آپلود شد", variant: "success" });
} catch (error) {
toast({
title: "آپلود ناموفق بود",
description: resolveErrorMessage(error, "دوباره تلاش کنید"),
variant: "destructive",
});
} finally {
setUploading(false);
}
};
const onFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
event.currentTarget.value = "";
if (file) void uploadAsset(file);
};
const copySnippet = async (asset: Types.PostAssetSchema) => {
const snippet = asset.markdown_image || asset.markdown_link || asset.absolute_file_url || "";
await navigator.clipboard.writeText(snippet);
toast({ title: "کد مارک‌داون کپی شد", variant: "success" });
};
const deleteAsset = async (assetId: number) => {
setDeletingId(assetId);
try {
await api.deleteBlogPostAsset(postId, assetId);
setAssets((prev) => prev.filter((asset) => asset.id !== assetId));
toast({ title: "فایل حذف شد", variant: "success" });
} catch (error) {
toast({
title: "حذف فایل ناموفق بود",
description: resolveErrorMessage(error, "دوباره تلاش کنید"),
variant: "destructive",
});
} finally {
setDeletingId(null);
}
};
if (loading) {
return (
<div className="flex min-h-[50vh] items-center justify-center" dir="rtl">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
</div>
);
}
return (
<div className="space-y-6">
<div className="flex flex-col gap-4 md:flex-row-reverse md:items-center md:justify-between">
<div className="text-right">
<h2 className="text-2xl font-bold">مرکز آپلود نوشته</h2>
<p className="mt-1 text-sm text-muted-foreground">
{post?.title ? `فایل‌های عمومی مرتبط با «${post.title}»` : "فایل‌های عمومی این نوشته را مدیریت کنید."}
</p>
</div>
<Button variant="outline" onClick={() => router.push(`/admin/blog/${postId}/edit`)}>
<ArrowRight className="ml-2 h-4 w-4" />
بازگشت به ویرایش
</Button>
</div>
<Card>
<CardHeader className="text-right">
<CardTitle>آپلود فایل</CardTitle>
<CardDescription>تصاویر، ویدئوها، اسناد و فایلهای فشرده مجاز هستند. لینک مارکداون هر فایل بعد از آپلود قابل کپی است.</CardDescription>
</CardHeader>
<CardContent className="space-y-5">
<input ref={fileInputRef} type="file" className="hidden" onChange={onFileChange} />
<Button variant="secondary" onClick={() => fileInputRef.current?.click()} disabled={uploading}>
{uploading ? <Loader2 className="ml-2 h-4 w-4 animate-spin" /> : <UploadCloud className="ml-2 h-4 w-4" />}
آپلود فایل
</Button>
{assets.length ? (
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{assets.map((asset) => (
<div key={asset.id} className="rounded-2xl border p-3">
<div className="flex flex-row-reverse items-start justify-between gap-3">
<div className="text-right">
<div className="flex flex-wrap items-center justify-end gap-2">
<Badge variant="secondary">{asset.file_type}</Badge>
<p className="font-medium">{asset.title}</p>
</div>
<p className="mt-1 text-xs text-muted-foreground">
{asset.mime_type || "file"} · {Math.ceil(asset.size / 1024)} KB
</p>
</div>
<div className="flex shrink-0 gap-1">
<Button variant="ghost" size="icon" onClick={() => copySnippet(asset)}>
<Copy className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="text-destructive hover:text-destructive"
onClick={() => deleteAsset(asset.id)}
disabled={deletingId === asset.id}
>
{deletingId === asset.id ? <Loader2 className="h-4 w-4 animate-spin" /> : <Trash2 className="h-4 w-4" />}
</Button>
</div>
</div>
{asset.absolute_preview_url ? (
<img src={asset.absolute_preview_url} alt={asset.alt_text || asset.title} className="mt-3 aspect-video w-full rounded-xl object-cover" />
) : asset.absolute_file_url ? (
<a className="mt-3 block truncate rounded-xl bg-muted px-3 py-2 text-left text-xs underline" href={asset.absolute_file_url} target="_blank" rel="noreferrer">
{asset.absolute_file_url}
</a>
) : null}
</div>
))}
</div>
) : (
<div className="rounded-2xl border border-dashed p-8 text-center text-sm text-muted-foreground">
هنوز فایلی برای این نوشته آپلود نشده است.
</div>
)}
</CardContent>
</Card>
</div>
);
}