feat(admin): file manager — search, type tabs (image/video/AE), library picker
- FileManager: type tabs (همه/تصاویر/ویدیوها/صدا/پروژههای AE و سایر) + name search (uses file_type + search params the file svc already supports; type values capitalized to match the enum), video thumbnails via <video>, AE/zip shown under "AE و سایر"; delete + copy-URL retained - FilePicker: reusable modal to re-choose an existing file from the library (search + filter + click to pick) - FileUploadField: new "از کتابخانه" button on every upload field → pick from library instead of re-uploading; picker auto-filters by the field's accept - shared src/lib/admin-files.ts helpers (fileUrl/isImage/isVideo/fetchFiles) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import { MINIO_PUBLIC_URL } from "@/lib/files";
|
||||
|
||||
export interface FileItem {
|
||||
id: string;
|
||||
name: string;
|
||||
mime_type?: string;
|
||||
file_type?: string; // Image | Video | Audio | Voiceover | Document | Other
|
||||
size_bytes?: number;
|
||||
minio_bucket?: string;
|
||||
minio_key?: string;
|
||||
cdn_url?: string | null;
|
||||
file_address?: string | null;
|
||||
thumbnail_url?: string | null;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
/** Type filter tabs — values are the exact (capitalized) FileKind enum the API expects. */
|
||||
export const FILE_TYPE_TABS: { v: string; l: string }[] = [
|
||||
{ v: "", l: "همه" },
|
||||
{ v: "Image", l: "تصاویر" },
|
||||
{ v: "Video", l: "ویدیوها" },
|
||||
{ v: "Audio", l: "صدا" },
|
||||
{ v: "Other", l: "پروژههای AE و سایر" },
|
||||
];
|
||||
|
||||
export function fileUrl(f: FileItem): string | null {
|
||||
if (f.cdn_url) return f.cdn_url;
|
||||
if (f.file_address) return f.file_address;
|
||||
if (f.minio_key) return `${MINIO_PUBLIC_URL}/${f.minio_bucket ?? "user-uploads"}/${f.minio_key}`;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isImage(f: FileItem): boolean {
|
||||
return f.file_type === "Image" || (f.mime_type ?? "").startsWith("image/") || /\.(png|jpe?g|gif|webp|svg|avif)$/i.test(f.name);
|
||||
}
|
||||
export function isVideo(f: FileItem): boolean {
|
||||
return f.file_type === "Video" || (f.mime_type ?? "").startsWith("video/") || /\.(mp4|webm|mov|m4v|mkv)$/i.test(f.name);
|
||||
}
|
||||
export function isAep(f: FileItem): boolean {
|
||||
return /\.(aep|aepx|zip)$/i.test(f.name);
|
||||
}
|
||||
|
||||
export function humanSize(n?: number): string {
|
||||
if (!n) return "—";
|
||||
const u = ["B", "KB", "MB", "GB"];
|
||||
let i = 0, v = n;
|
||||
while (v >= 1024 && i < u.length - 1) { v /= 1024; i++; }
|
||||
return `${v.toFixed(v < 10 && i > 0 ? 1 : 0)} ${u[i]}`;
|
||||
}
|
||||
|
||||
/** Fetch files from the admin gateway with optional search + type filter. */
|
||||
export async function fetchFiles(search: string, fileType: string, page = 1, pageSize = 60): Promise<FileItem[]> {
|
||||
const qs = new URLSearchParams({ page: String(page), pageSize: String(pageSize), page_size: String(pageSize) });
|
||||
if (search) qs.set("search", search);
|
||||
if (fileType) qs.set("file_type", fileType);
|
||||
const r = await fetch(`/api/admin/resource/files?${qs.toString()}`, { cache: "no-store" }).then((x) => x.json()).catch(() => null);
|
||||
return r?.items ?? r?.data ?? (Array.isArray(r) ? r : []);
|
||||
}
|
||||
Reference in New Issue
Block a user