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:
soroush.asadi
2026-06-03 01:11:47 +03:30
parent ffc0c5d6d5
commit e5812488eb
4 changed files with 193 additions and 37 deletions
+58
View File
@@ -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 : []);
}