diff --git a/src/components/admin/FileManager.tsx b/src/components/admin/FileManager.tsx index fb0223f..5cdcb6a 100644 --- a/src/components/admin/FileManager.tsx +++ b/src/components/admin/FileManager.tsx @@ -2,56 +2,31 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import { MINIO_PUBLIC_URL } from "@/lib/files"; - -interface FileItem { - id: string; - name: string; - mime_type?: string; - file_type?: string; - size_bytes?: number; - minio_bucket?: string; - minio_key?: string; - created_at?: string; -} +import { FILE_TYPE_TABS, fetchFiles, fileUrl, humanSize, isImage, isVideo, type FileItem } from "@/lib/admin-files"; const card = "rounded-xl border border-[#1e2235] bg-[#0f1120]"; const btn = "rounded-lg bg-indigo-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-indigo-500 disabled:opacity-50"; -function fileUrl(f: FileItem): string | null { - if (!f.minio_key) return null; - return `${MINIO_PUBLIC_URL}/${f.minio_bucket ?? "user-uploads"}/${f.minio_key}`; -} -function isImage(f: FileItem): boolean { - return (f.mime_type ?? "").startsWith("image/") || /\.(png|jpe?g|gif|webp|svg|avif)$/i.test(f.name); -} -function humanSize(n?: number): string { - if (!n) return "—"; - const u = ["B", "KB", "MB", "GB"]; let i = 0; let v = n; - while (v >= 1024 && i < u.length - 1) { v /= 1024; i++; } - return `${v.toFixed(v < 10 && i > 0 ? 1 : 0)} ${u[i]}`; -} - export function FileManager() { const [files, setFiles] = useState([]); const [loading, setLoading] = useState(true); const [uploading, setUploading] = useState(false); const [error, setError] = useState(null); const [copied, setCopied] = useState(null); + const [search, setSearch] = useState(""); + const [type, setType] = useState(""); const inputRef = useRef(null); const reload = useCallback(async () => { setLoading(true); try { - const res = await fetch("/api/admin/resource/files", { cache: "no-store" }); - const data = await res.json(); - setFiles(data?.data ?? data?.items ?? (Array.isArray(data) ? data : [])); + setFiles(await fetchFiles(search, type)); } catch { setError("بارگذاری فایل‌ها ناموفق بود"); } finally { setLoading(false); } - }, []); + }, [search, type]); useEffect(() => { reload(); }, [reload]); @@ -85,16 +60,31 @@ export function FileManager() { }; return ( -
-
+
+

کتابخانه رسانه

-

آپلود و مدیریت تصاویر و فایل‌ها. نشانی‌های عمومی را می‌توان در هر فیلدی کپی کرد.

+

آپلود و مدیریت تصاویر، ویدیوها و پروژه‌های افترافکت. نشانی هر فایل را می‌توان در فرم‌ها استفاده کرد.

- e.target.files && uploadFiles(e.target.files)} /> + e.target.files && uploadFiles(e.target.files)} /> +
+ +
+
+ {FILE_TYPE_TABS.map((t) => ( + + ))} +
+ setSearch(e.target.value)} + />
{error &&

{error}

} @@ -107,7 +97,7 @@ export function FileManager() { {loading ? (

در حال بارگذاری…

) : files.length === 0 ? ( -

هنوز فایلی وجود ندارد. فایل را اینجا بکشید و رها کنید یا روی آپلود بزنید.

+

فایلی یافت نشد. فایل را اینجا بکشید و رها کنید یا روی آپلود بزنید.

) : (
{files.map((f) => { @@ -118,12 +108,14 @@ export function FileManager() { {url && isImage(f) ? ( // eslint-disable-next-line @next/next/no-img-element {f.name} + ) : url && isVideo(f) ? ( +

{f.name}

-

{humanSize(f.size_bytes)}

+

{f.file_type ?? "—"} · {humanSize(f.size_bytes)}

{url && ( +
+
+ {FILE_TYPE_TABS.map((t) => ( + + ))} +
+
+ {loading ? ( +

در حال بارگذاری…

+ ) : files.length === 0 ? ( +

فایلی یافت نشد.

+ ) : ( +
+ {files.map((f) => { + const url = fileUrl(f); + return ( + + ); + })} +
+ )} +
+
+
+ ); +} diff --git a/src/components/admin/FileUploadField.tsx b/src/components/admin/FileUploadField.tsx index ff27ca4..6ef4c25 100644 --- a/src/components/admin/FileUploadField.tsx +++ b/src/components/admin/FileUploadField.tsx @@ -2,6 +2,8 @@ import { useRef, useState } from "react"; +import { FilePicker } from "@/components/admin/FilePicker"; + /** Upload-or-clear field that replaces a raw URL text input. Stores the public URL. */ export function FileUploadField({ value, @@ -17,6 +19,12 @@ export function FileUploadField({ const inputRef = useRef(null); const [uploading, setUploading] = useState(false); const [error, setError] = useState(null); + const [pickOpen, setPickOpen] = useState(false); + + // Map the accept hint to a library type filter (image / video / AE-and-other). + const pickerType = accept.includes("image") && !accept.includes("video") ? "Image" + : accept.includes("video") ? "Video" + : /\.aep|\.zip/.test(accept) ? "Other" : ""; const isImage = value && /\.(png|jpe?g|gif|webp|svg|avif)$/i.test(value); @@ -75,6 +83,13 @@ export function FileUploadField({ > {uploading ? "در حال بارگذاری…" : value ? "تعویض" : "بارگذاری"} + {value && (
{error &&

{error}

} + setPickOpen(false)} + onSelect={(url) => { onChange(url); setPickOpen(false); }} + /> ); } diff --git a/src/lib/admin-files.ts b/src/lib/admin-files.ts new file mode 100644 index 0000000..fac7757 --- /dev/null +++ b/src/lib/admin-files.ts @@ -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 { + 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 : []); +}