diff --git a/components/utilities/files/FileBrowserPanel.tsx b/components/utilities/files/FileBrowserPanel.tsx index 7262cdc6..dbe39eb0 100644 --- a/components/utilities/files/FileBrowserPanel.tsx +++ b/components/utilities/files/FileBrowserPanel.tsx @@ -1,37 +1,15 @@ "use client"; -import React, { - useEffect, - useMemo, - useState, - useCallback, - useRef, - MutableRefObject, -} from "react"; -import { - Loader2, - Download, - Folder, - FileText, - RefreshCw, - ArrowUp, - Home, -} from "lucide-react"; - -type FileItem = { - name: string; - type: "dir" | "file"; - size?: number; - mtimeMs?: number; -}; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { Loader2, Download, Folder, FileText, RefreshCw, ArrowUp, Home } from "lucide-react"; +type FileItem = { name: string; type: "dir" | "file"; size?: number; mtimeMs?: number }; type ListResponse = | { path: string; items: FileItem[] } | { path: string; entries: FileItem[] } | { items?: FileItem[]; entries?: FileItem[]; path?: string }; -const BASE = -(process.env.NEXT_PUBLIC_FILE_API_BASE_URL || "").replace(/\/$/, "") || ""; +const BASE = (process.env.NEXT_PUBLIC_FILE_API_BASE_URL || "").replace(/\/$/, "") || ""; function joinPath(a: string, b: string) { if (!a || a === "/") return b.startsWith("/") ? b : `/${b}`; @@ -49,14 +27,15 @@ function formatSize(bytes?: number) { const units = ["B", "KB", "MB", "GB", "TB"]; let v = bytes; let u = 0; - while (v >= 1024 && u < units.length - 1) { - v /= 1024; - u++; - } + while (v >= 1024 && u < units.length - 1) { v /= 1024; u++; } return `${v.toFixed(u ? 1 : 0)} ${units[u]}`; } - -const LS_KEY = "fs.split.left"; // px +function formatDate(ms?: number) { + if (!ms) return "—"; + const d = new Date(ms); + // date only, short & compact + return d.toLocaleDateString(undefined, { year: "numeric", month: "numeric", day: "numeric" }); +} export default function FileBrowserPanel() { const [path, setPath] = useState("/"); @@ -65,38 +44,6 @@ export default function FileBrowserPanel() { const [error, setError] = useState(null); const [previewHref, setPreviewHref] = useState(null); - // ───────────────── Splitter state - const containerRef = useRef(null); - const [leftPx, setLeftPx] = useState(() => { - if (typeof window === "undefined") return 640; // SSR default - const v = Number(localStorage.getItem(LS_KEY)); - return Number.isFinite(v) && v > 0 ? v : 720; // good desktop start - }); - - // Enforce bounds on resize / first mount - const clampLeft = useCallback((want: number) => { - const host = containerRef.current; - if (!host) return want; - const total = host.clientWidth || 0; - const MIN_LEFT = 520; // so Name + columns fit - const MIN_RIGHT = 360; // keep preview usable - const maxLeft = Math.max(320, total - MIN_RIGHT); - return Math.min(Math.max(want, MIN_LEFT), maxLeft); - }, []); - - useEffect(() => { - const handle = () => setLeftPx((v) => clampLeft(v)); - window.addEventListener("resize", handle); - return () => window.removeEventListener("resize", handle); - }, [clampLeft]); - - useEffect(() => { - if (typeof window !== "undefined") { - localStorage.setItem(LS_KEY, String(leftPx)); - } - }, [leftPx]); - - // ───────────────── Data const urlList = useMemo(() => { const p = encodeURIComponent(path || "/"); return `${BASE}/api/files/list?path=${p}`; @@ -115,23 +62,12 @@ export default function FileBrowserPanel() { setError(null); setPreviewHref(null); try { - const res = await fetch(urlList, { - method: "GET", - credentials: "include", - headers: { Accept: "application/json" }, - cache: "no-store", - }); - if (!res.ok) { - const text = await res.text().catch(() => ""); - throw new Error(text || `HTTP ${res.status}`); - } + const res = await fetch(urlList, { method: "GET", credentials: "include", headers: { Accept: "application/json" }, cache: "no-store" }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); const json: ListResponse = await res.json(); const arr = (json as any).items || (json as any).entries || []; if (!Array.isArray(arr)) throw new Error("Malformed list response"); - arr.sort((a: FileItem, b: FileItem) => { - if (a.type !== b.type) return a.type === "dir" ? -1 : 1; - return a.name.localeCompare(b.name); - }); + arr.sort((a: FileItem, b: FileItem) => (a.type !== b.type ? (a.type === "dir" ? -1 : 1) : a.name.localeCompare(b.name))); setItems(arr); } catch (e: any) { setError(e?.message || String(e)); @@ -141,104 +77,38 @@ export default function FileBrowserPanel() { } }, [urlList]); - useEffect(() => { - fetchList(); - }, [fetchList]); + useEffect(() => { fetchList(); }, [fetchList]); - // ───────────────── Actions const onOpen = (it: FileItem) => { - if (it.type === "dir") { - setPath((p) => joinPath(p, it.name)); - } else { - setPreviewHref(urlRaw(joinPath(path, it.name))); - } + if (it.type === "dir") setPath((p) => joinPath(p, it.name)); + else setPreviewHref(urlRaw(joinPath(path, it.name))); }; - const onUp = () => setPath((p) => parentPath(p)); - const onHome = () => setPath("/"); - const onDownload = (it: FileItem) => { - const href = urlDownload(joinPath(path, it.name)); - const a = document.createElement("a"); - a.href = href; - a.download = it.name; - document.body.appendChild(a); - a.click(); - a.remove(); - }; - - // ───────────────── Splitter drag handlers - const draggingRef = useRef(false); - const startXRef = useRef(0); - const startLeftRef = useRef(0); - - const onSplitDown = (e: React.MouseEvent | React.TouchEvent) => { - draggingRef.current = true; - startXRef.current = - "touches" in e ? e.touches[0].clientX : (e as React.MouseEvent).clientX; - startLeftRef.current = leftPx; - document.addEventListener("mousemove", onSplitMove as any); - document.addEventListener("mouseup", onSplitUp as any); - document.addEventListener("touchmove", onSplitMove as any, { passive: false }); - document.addEventListener("touchend", onSplitUp as any); - }; - - const onSplitMove = (e: MouseEvent | TouchEvent) => { - if (!draggingRef.current) return; - const clientX = - e instanceof TouchEvent ? e.touches[0]?.clientX ?? startXRef.current : e.clientX; - const dx = clientX - startXRef.current; - setLeftPx((prev) => clampLeft(startLeftRef.current + dx)); - if (e.cancelable) e.preventDefault(); - }; - - const onSplitUp = () => { - draggingRef.current = false; - document.removeEventListener("mousemove", onSplitMove as any); - document.removeEventListener("mouseup", onSplitUp as any); - document.removeEventListener("touchmove", onSplitMove as any); - document.removeEventListener("touchend", onSplitUp as any); + const onUp = () => setPath((p) => parentPath(p)); + const onHome = () => setPath("/"); + const onDownload = (it: FileItem) => { + const href = urlDownload(joinPath(path, it.name)); + const a = document.createElement("a"); + a.href = href; a.download = it.name; document.body.appendChild(a); a.click(); a.remove(); }; return (
- {/* Small styles */} + {/* keep typography consistent */} {/* Toolbar */}
- -
- -
@@ -246,28 +116,25 @@ export default function FileBrowserPanel() {
{path || "/"}
- {/* Split container */} -
- {/* LEFT: table */} -
-
-
- {/* Header: name flexes; others compact so Actions never truncates */} -
+ {/* Two-column layout: left auto, right fixed min (no horizontal scroll in left) */} +
+ {/* LEFT: table (no horizontal scrolling; columns sized to fit) */} +
+ {/* Header */} +
Name
-
Type
-
Size
-
Modified
+
Type
+
Size
+
Date
Actions
+ {/* Rows */}
{loading ? (
@@ -279,34 +146,33 @@ export default function FileBrowserPanel() {
Empty folder.
) : ( items.map((it) => ( -
+
+ {/* Name (truncate, tooltipped) */} -
- {it.type} -
-
{formatSize(it.size)}
-
- {it.mtimeMs ? new Date(it.mtimeMs).toLocaleString() : "—"} -
+ + {/* Type */} +
{it.type}
+ + {/* Size */} +
{formatSize(it.size)}
+ + {/* Date (no time) */} +
{formatDate(it.mtimeMs)}
+ + {/* Actions (fixed width, never wraps) */}
{it.type === "file" && (
-
-
- {/* GUTTER */} -
- - {/* RIGHT: preview */} -
-
+ {/* RIGHT: preview (fixed min width; grows with viewport) */} +
Preview
-
+
{previewHref ? ( -