makearmy-app/app/files/page.tsx
2025-09-22 10:37:53 -04:00

135 lines
4 KiB
TypeScript

// /var/www/makearmy.io/app/app/files/page.tsx
"use client";
import Link from "next/link";
import { useEffect, useMemo, useState } from "react";
import { useSearchParams, useRouter } from "next/navigation";
type FileItem = {
name: string;
isDir: boolean;
size: number;
mtime: number;
};
export default function FilesPage() {
const search = useSearchParams();
const router = useRouter();
const path = useMemo(() => search.get("path") || "/", [search]);
const [items, setItems] = useState<FileItem[] | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
async function load() {
setLoading(true);
setError(null);
try {
const res = await fetch(
`/api/files/list?path=${encodeURIComponent(path)}`
);
if (!res.ok) {
if (!cancelled) setError(`HTTP ${res.status}`);
return;
}
const json = await res.json();
if (!cancelled) setItems(json.items || []);
} catch (e: any) {
if (!cancelled) setError(e?.message || String(e));
} finally {
if (!cancelled) setLoading(false);
}
}
load();
return () => {
cancelled = true;
};
}, [path]);
const upPath = useMemo(() => {
if (path === "/") return null;
const parts = path.replace(/\/+$/, "").split("/").filter(Boolean);
parts.pop();
return "/" + parts.join("/");
}, [path]);
return (
<div className="p-6 text-sm">
<div className="mb-3">
<span className="opacity-70 mr-1">Path:</span>
<code>{path}</code>
{upPath && (
<>
<span className="mx-2 opacity-50"></span>
<Link href={`/files?path=${encodeURIComponent(upPath)}`}>
Up one level
</Link>
</>
)}
</div>
{loading && <div>Loading</div>}
{error && (
<div className="bg-red-900/60 text-red-200 p-3 rounded border border-red-800">
Error loading files: {error}
</div>
)}
{!loading && !error && items && (
<table className="w-full text-left mt-3 border-collapse">
<thead className="opacity-70">
<tr>
<th className="py-2 pr-4">Name</th>
<th className="py-2 pr-4">Type</th>
<th className="py-2 pr-4">Size</th>
<th className="py-2 pr-4">Modified</th>
<th className="py-2 pr-4"></th>
</tr>
</thead>
<tbody>
{items.map((it) => {
const href = it.isDir
? `/files?path=${encodeURIComponent(
(path.endsWith("/") ? path : path + "/") + it.name
)}`
: `/api/files/raw?path=${encodeURIComponent(
(path.endsWith("/") ? path : path + "/") + it.name
)}`;
const dl = it.isDir
? null
: `/api/files/download?path=${encodeURIComponent(
(path.endsWith("/") ? path : path + "/") + it.name
)}`;
return (
<tr key={it.name} className="border-t border-white/10">
<td className="py-2 pr-4">
<Link href={href}>{it.name}</Link>
</td>
<td className="py-2 pr-4">{it.isDir ? "Dir" : "File"}</td>
<td className="py-2 pr-4">
{it.isDir ? "-" : `${it.size.toLocaleString()} B`}
</td>
<td className="py-2 pr-4">
{new Date(it.mtime).toLocaleString()}
</td>
<td className="py-2 pr-4">
{!it.isDir && dl && (
<a href={dl} className="underline">
Download
</a>
)}
</td>
</tr>
);
})}
</tbody>
</table>
)}
</div>
);
}