112 lines
4.1 KiB
TypeScript
112 lines
4.1 KiB
TypeScript
// components/utilities/files/FilesTable.tsx
|
|
"use client";
|
|
|
|
import { useMemo } from "react";
|
|
import { ArrowDownAZ, ArrowUpAZ, Download, Folder, FileText } from "lucide-react";
|
|
import { FsEntry, SortKey, SortDir, nicelyFormatBytes } from "./api";
|
|
|
|
export default function FilesTable({
|
|
entries,
|
|
sortKey,
|
|
sortDir,
|
|
onSort,
|
|
onOpen,
|
|
onDownload,
|
|
}: {
|
|
entries: FsEntry[];
|
|
sortKey: SortKey;
|
|
sortDir: SortDir;
|
|
onSort: (k: SortKey) => void;
|
|
onOpen: (entry: FsEntry) => void;
|
|
onDownload: (entry: FsEntry) => void;
|
|
}) {
|
|
const sorted = useMemo(() => {
|
|
const arr = [...entries];
|
|
const dir = sortDir === "asc" ? 1 : -1;
|
|
arr.sort((a, b) => {
|
|
// folders first
|
|
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
|
|
switch (sortKey) {
|
|
case "name": return a.name.localeCompare(b.name) * dir;
|
|
case "size": return ((a.size ?? -1) - (b.size ?? -1)) * dir;
|
|
case "modified": return (new Date(a.modified || 0).getTime() - new Date(b.modified || 0).getTime()) * dir;
|
|
case "type": {
|
|
const ax = ext(a.name), bx = ext(b.name);
|
|
return ax.localeCompare(bx) * dir;
|
|
}
|
|
}
|
|
});
|
|
return arr;
|
|
}, [entries, sortKey, sortDir]);
|
|
|
|
function ext(name: string) {
|
|
const m = /\.([^.]+)$/.exec(name || "");
|
|
return m ? m[1].toLowerCase() : "";
|
|
}
|
|
|
|
function SortBtn({ k, label }: { k: SortKey; label: string }) {
|
|
const active = sortKey === k;
|
|
return (
|
|
<button className="inline-flex items-center gap-1 text-left" onClick={() => onSort(k)}>
|
|
{label}
|
|
{active ? (
|
|
sortDir === "asc" ? <ArrowUpAZ className="w-3.5 h-3.5 opacity-60" /> : <ArrowDownAZ className="w-3.5 h-3.5 opacity-60" />
|
|
) : null}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="overflow-auto rounded-md border">
|
|
<table className="w-full text-sm">
|
|
<thead className="text-left text-zinc-400 bg-muted/40">
|
|
<tr>
|
|
<th className="px-3 py-2 w-[40%]"><SortBtn k="name" label="Name" /></th>
|
|
<th className="px-3 py-2 w-[15%]"><SortBtn k="type" label="Type" /></th>
|
|
<th className="px-3 py-2 w-[15%]"><SortBtn k="size" label="Size" /></th>
|
|
<th className="px-3 py-2 w-[30%]"><SortBtn k="modified" label="Modified" /></th>
|
|
<th className="px-3 py-2 text-right">Get</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{sorted.map((e) => (
|
|
<tr key={e.path} className="border-t hover:bg-muted/30">
|
|
<td
|
|
className="px-3 py-2 cursor-pointer"
|
|
onDoubleClick={() => onOpen(e)}
|
|
title="Double-click to open"
|
|
>
|
|
<span className="inline-flex items-center gap-2">
|
|
{e.isDir ? <Folder className="w-4 h-4 opacity-70" /> : <FileText className="w-4 h-4 opacity-70" />}
|
|
<span className="truncate">{e.name}</span>
|
|
</span>
|
|
</td>
|
|
<td className="px-3 py-2">{e.isDir ? "Folder" : (e.mime || ext(e.name).toUpperCase() || "File")}</td>
|
|
<td className="px-3 py-2">{e.isDir ? "—" : nicelyFormatBytes(e.size)}</td>
|
|
<td className="px-3 py-2">{e.modified ? new Date(e.modified).toLocaleString() : "—"}</td>
|
|
<td className="px-3 py-2">
|
|
{!e.isDir && (
|
|
<div className="flex justify-end">
|
|
<button
|
|
className="rounded-md border px-2 py-1 text-xs hover:bg-muted inline-flex items-center gap-1"
|
|
onClick={() => onDownload(e)}
|
|
>
|
|
<Download className="w-3.5 h-3.5" />
|
|
Download
|
|
</button>
|
|
</div>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
{sorted.length === 0 && (
|
|
<tr>
|
|
<td className="px-3 py-6 text-sm text-zinc-500" colSpan={5}>Empty folder.</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
);
|
|
}
|