changes
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api, type Business, type BusinessFile } from '../api.js';
|
||||
import {
|
||||
api,
|
||||
businessFileUrl,
|
||||
viewerUrl,
|
||||
type Business,
|
||||
type BusinessFile,
|
||||
} from '../api.js';
|
||||
|
||||
function formatSize(bytes: number): string {
|
||||
if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
||||
@@ -16,61 +22,108 @@ function formatDate(iso: string): string {
|
||||
});
|
||||
}
|
||||
|
||||
const isPdf = (filePath: string) => filePath.toLowerCase().endsWith('.pdf');
|
||||
|
||||
export default function BusinessDetail({ id, onBack }: { id: string; onBack: () => void }) {
|
||||
const [business, setBusiness] = useState<Business | null>(null);
|
||||
const [files, setFiles] = useState<BusinessFile[] | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selected, setSelected] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setSelected(null);
|
||||
api.business(id).then(setBusiness).catch((err: Error) => setError(err.message));
|
||||
api.businessFiles(id).then(setFiles).catch((err: Error) => setError(err.message));
|
||||
}, [id]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={onBack} className="mb-4 text-sm text-blue-600 hover:underline">
|
||||
← Back to businesses
|
||||
</button>
|
||||
<div className="flex h-full min-h-0 flex-col">
|
||||
<div className="mb-3 shrink-0">
|
||||
<button onClick={onBack} className="text-sm text-blue-600 hover:underline">
|
||||
← Back to businesses
|
||||
</button>
|
||||
|
||||
{error && <p className="mb-3 text-sm text-red-600">{error}</p>}
|
||||
{error && <p className="mt-2 text-sm text-red-600">{error}</p>}
|
||||
|
||||
{business && (
|
||||
<div className="mb-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-lg font-semibold">{business.name}</h1>
|
||||
<span className="rounded-full border border-gray-300 bg-white px-2 py-0.5 text-xs text-gray-600">
|
||||
{business.status}
|
||||
</span>
|
||||
{business && (
|
||||
<div className="mt-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-lg font-semibold">{business.name}</h1>
|
||||
<span className="rounded-full border border-gray-300 bg-white px-2 py-0.5 text-xs text-gray-600">
|
||||
{business.status}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 font-mono text-xs text-gray-500">{business.nas_path}</p>
|
||||
</div>
|
||||
<p className="mt-1 font-mono text-xs text-gray-500">{business.nas_path}</p>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
|
||||
<table className="w-full border-collapse bg-white text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200 text-left text-xs uppercase tracking-wide text-gray-500">
|
||||
<th className="px-3 py-2 font-medium">File</th>
|
||||
<th className="w-24 px-3 py-2 font-medium">Size</th>
|
||||
<th className="w-48 px-3 py-2 font-medium">Modified</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{files?.map((f) => (
|
||||
<tr key={f.name} className="border-b border-gray-100">
|
||||
<td className="px-3 py-1.5">{f.name}</td>
|
||||
<td className="px-3 py-1.5 text-gray-500">{formatSize(f.size)}</td>
|
||||
<td className="px-3 py-1.5 text-gray-500">{formatDate(f.mtime)}</td>
|
||||
</tr>
|
||||
))}
|
||||
{files && files.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={3} className="px-3 py-4 text-gray-500">
|
||||
No files in this directory.
|
||||
</td>
|
||||
</tr>
|
||||
{/* Master-detail: file table left, PDF viewer right, together filling the viewport. */}
|
||||
<div className="flex min-h-0 flex-1 gap-4">
|
||||
<div className="min-w-0 flex-1 overflow-auto rounded border border-gray-200 bg-white">
|
||||
<table className="w-full border-collapse text-sm">
|
||||
<thead className="sticky top-0 bg-white">
|
||||
<tr className="border-b border-gray-200 text-left text-xs uppercase tracking-wide text-gray-500">
|
||||
<th className="px-3 py-2 font-medium">File</th>
|
||||
<th className="w-24 px-3 py-2 font-medium">Size</th>
|
||||
<th className="w-44 px-3 py-2 font-medium">Modified</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{files?.map((f) => (
|
||||
<tr
|
||||
key={f.path}
|
||||
onClick={isPdf(f.path) ? () => setSelected(f.path) : undefined}
|
||||
className={`border-b border-gray-100 ${
|
||||
isPdf(f.path) ? 'cursor-pointer hover:bg-gray-50' : ''
|
||||
} ${f.path === selected ? 'bg-blue-50 hover:bg-blue-50' : ''}`}
|
||||
>
|
||||
<td className="px-3 py-1.5">
|
||||
{isPdf(f.path) ? (
|
||||
<span className={f.path === selected ? 'font-medium text-blue-700' : ''}>
|
||||
{f.path}
|
||||
</span>
|
||||
) : (
|
||||
<a
|
||||
href={businessFileUrl(id, f.path)}
|
||||
className="text-blue-600 hover:underline"
|
||||
>
|
||||
{f.path}
|
||||
</a>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-1.5 text-gray-500">{formatSize(f.size)}</td>
|
||||
<td className="px-3 py-1.5 text-gray-500">{formatDate(f.mtime)}</td>
|
||||
</tr>
|
||||
))}
|
||||
{files && files.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={3} className="px-3 py-4 text-gray-500">
|
||||
No files in this directory.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1 overflow-hidden rounded border border-gray-200 bg-gray-100">
|
||||
{selected ? (
|
||||
<iframe
|
||||
// Remounting on `key` change is what makes the standalone page reload
|
||||
// the new document — it reads ?file= once at startup.
|
||||
key={selected}
|
||||
src={viewerUrl(id, selected)}
|
||||
title={selected}
|
||||
className="h-full w-full border-0"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-sm text-gray-500">
|
||||
Select a PDF to view it here.
|
||||
</div>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user