MicProtocol / apps /web /src /components /ui /documents /DocumentLibrary.tsx
Benaah's picture
Upload folder using huggingface_hub
fb12607 verified
Raw History Blame Contribute Delete
12.7 kB
"use client";
import { useCallback, useMemo, useState } from "react";
import {
Download,
FileText,
Image,
Share2,
Trash2,
ShieldAlert,
Clock,
RefreshCw,
MoreHorizontal,
} from "lucide-react";
import { trpc } from "@/lib/trpc";
import { useToast } from "@/lib/toast";
import { DocumentStatus } from "@micprotocol/shared-types";
import { Button } from "@micprotocol/ui";
import { SectionHeader } from "@micprotocol/ui";
import { EmptyState } from "@micprotocol/ui";
import { Badge } from "@micprotocol/ui";
function DocumentTypeIcon({ mimeType }: { mimeType: string }) {
if (mimeType === "application/pdf") return <FileText size={16} />;
if (mimeType.startsWith("image/")) return <Image size={16} />;
return <FileText size={16} />;
}
function StatusBadge({ status }: { status: DocumentStatus }) {
const config: Record<string, { color: string; bg: string }> = {
draft: { color: "var(--text-tertiary)", bg: "var(--bg-tertiary)" },
active: { color: "var(--color-success)", bg: "color-mix(in srgb, var(--color-success) 12%, transparent)" },
revoked: { color: "var(--color-error)", bg: "color-mix(in srgb, var(--color-error) 12%, transparent)" },
flagged: { color: "var(--color-warning)", bg: "color-mix(in srgb, var(--color-warning) 12%, transparent)" },
quarantined: { color: "var(--color-destructive)", bg: "color-mix(in srgb, var(--color-destructive) 12%, transparent)" },
};
const c = config[status] || config.draft;
return (
<span
style={{
display: "inline-flex",
alignItems: "center",
gap: 4,
padding: "2px 8px",
borderRadius: "var(--radius-full)",
background: c.bg,
color: c.color,
fontSize: "var(--text-xs)",
fontWeight: 600,
textTransform: "capitalize",
}}
>
<span style={{ width: 6, height: 6, borderRadius: "50%", background: c.color }} /> {status}
</span>
);
}
interface DocumentLibraryProps {
workspaceId?: string;
}
export default function DocumentLibrary({ workspaceId }: DocumentLibraryProps) {
const [search, setSearch] = useState("");
const [statusFilter, setStatusFilter] = useState<DocumentStatus | undefined>(undefined);
const { success, error } = useToast();
const { data, isLoading, refetch } = trpc.documents.list.useQuery(
{
search: search || undefined,
status: statusFilter,
page: 1,
limit: 50,
},
{ retry: false, refetchOnWindowFocus: false }
);
const removeDocument = trpc.documents.delete.useMutation({
onSuccess: () => {
refetch();
success("Document deleted");
},
onError: (err) => {
error(err.message || "Failed to delete document");
},
});
const documents = data?.items ?? [];
const total = data?.total ?? 0;
const handleDelete = useCallback(
(id: string) => {
if (window.confirm("Are you sure you want to delete this document?")) {
removeDocument.mutate({ documentId: id });
}
},
[removeDocument]
);
return (
<div className="animate-fade-in space-y-6">
<SectionHeader
title="Document Library"
description="Manage, share, and track documents across your workspace"
/>
{/* KPI Cards */}
<div style={{ display: "grid", gap: "var(--space-4)", gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))" }}>
<div className="card" style={{ padding: "var(--space-4)" }}>
<div style={{ fontSize: "var(--text-xs)", color: "var(--text-tertiary)", textTransform: "uppercase", letterSpacing: "0.05em", marginBottom: "var(--space-2)" }}>
Total Documents
</div>
<div style={{ fontSize: "var(--text-2xl)", fontWeight: 700, color: "var(--text-primary)" }}>{total}</div>
</div>
<div className="card" style={{ padding: "var(--space-4)" }}>
<div style={{ fontSize: "var(--text-xs)", color: "var(--text-tertiary)", textTransform: "uppercase", letterSpacing: "0.05em", marginBottom: "var(--space-2)" }}>
Active
</div>
<div style={{ fontSize: "var(--text-2xl)", fontWeight: 700, color: "var(--color-success)" }}>
{documents.filter((d) => d.status === "active").length}
</div>
</div>
<div className="card" style={{ padding: "var(--space-4)" }}>
<div style={{ fontSize: "var(--text-xs)", color: "var(--text-tertiary)", textTransform: "uppercase", letterSpacing: "0.05em", marginBottom: "var(--space-2)" }}>
Flagged
</div>
<div style={{ fontSize: "var(--text-2xl)", fontWeight: 700, color: "var(--color-warning)" }}>
{documents.filter((d) => d.isForgeryFlagged).length}
</div>
</div>
<div className="card" style={{ padding: "var(--space-4)" }}>
<div style={{ fontSize: "var(--text-xs)", color: "var(--text-tertiary)", textTransform: "uppercase", letterSpacing: "0.05em", marginBottom: "var(--space-2)" }}>
Quarantined
</div>
<div style={{ fontSize: "var(--text-2xl)", fontWeight: 700, color: "var(--color-destructive)" }}>
{documents.filter((d) => d.status === "quarantined").length}
</div>
</div>
</div>
{/* Document List */}
<div className="card" style={{ overflow: "hidden" }}>
<div style={{ padding: "var(--space-4)", borderBottom: "1px solid var(--border-color)" }}>
<div style={{ display: "flex", alignItems: "center", gap: "var(--space-3)" }}>
<input
className="input"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search documents..."
style={{ flex: 1, fontSize: "var(--text-sm)", padding: "var(--space-2) var(--space-3)" }}
/>
<select
className="input"
value={statusFilter ?? ""}
onChange={(e) => setStatusFilter((e.target.value || undefined) as DocumentStatus | undefined)}
style={{ fontSize: "var(--text-sm)", padding: "var(--space-2) var(--space-3)" }}
>
<option value="">All Statuses</option>
<option value="draft">Draft</option>
<option value="active">Active</option>
<option value="revoked">Revoked</option>
<option value="flagged">Flagged</option>
<option value="quarantined">Quarantined</option>
</select>
<Button variant="outline" size="sm" onClick={() => refetch()} disabled={isLoading}>
<RefreshCw size={14} />
</Button>
</div>
</div>
<div style={{ padding: "var(--space-0)" }}>
{isLoading && (
<div style={{ padding: "var(--space-6)", textAlign: "center", color: "var(--text-tertiary)" }}>
Loading documents...
</div>
)}
{!isLoading && documents.length === 0 && (
<EmptyState
icon={<FileText size={32} />}
title="No documents yet"
subtitle="Upload your first document to get started"
/>
)}
{!isLoading && documents.length > 0 && (
<table style={{ width: "100%", borderCollapse: "collapse" }}>
<thead>
<tr style={{ borderBottom: "1px solid var(--border-color)" }}>
<th style={{ padding: "var(--space-3) var(--space-4)", textAlign: "left", fontSize: "var(--text-xs)", fontWeight: 600, color: "var(--text-tertiary)", textTransform: "uppercase", letterSpacing: "0.05em" }}>Document</th>
<th style={{ padding: "var(--space-3) var(--space-4)", textAlign: "left", fontSize: "var(--text-xs)", fontWeight: 600, color: "var(--text-tertiary)", textTransform: "uppercase", letterSpacing: "0.05em" }}>Type</th>
<th style={{ padding: "var(--space-3) var(--space-4)", textAlign: "left", fontSize: "var(--text-xs)", fontWeight: 600, color: "var(--text-tertiary)", textTransform: "uppercase", letterSpacing: "0.05em" }}>Size</th>
<th style={{ padding: "var(--space-3) var(--space-4)", textAlign: "left", fontSize: "var(--text-xs)", fontWeight: 600, color: "var(--text-tertiary)", textTransform: "uppercase", letterSpacing: "0.05em" }}>Status</th>
<th style={{ padding: "var(--space-3) var(--space-4)", textAlign: "left", fontSize: "var(--text-xs)", fontWeight: 600, color: "var(--text-tertiary)", textTransform: "uppercase", letterSpacing: "0.05em" }}>Shares</th>
<th style={{ padding: "var(--space-3) var(--space-4)", textAlign: "left", fontSize: "var(--text-xs)", fontWeight: 600, color: "var(--text-tertiary)", textTransform: "uppercase", letterSpacing: "0.05em" }}>Actions</th>
</tr>
</thead>
<tbody>
{documents.map((doc: any) => (
<tr key={doc.id} style={{ borderBottom: "1px solid var(--border-color-subtle)" }}>
<td style={{ padding: "var(--space-3) var(--space-4)" }}>
<div style={{ display: "flex", alignItems: "center", gap: "var(--space-3)" }}>
<div style={{ width: 36, height: 36, borderRadius: "var(--radius-md)", background: "var(--bg-tertiary)", display: "flex", alignItems: "center", justifyContent: "center" }}>
<DocumentTypeIcon mimeType={doc.mimeType} />
</div>
<div>
<div style={{ fontWeight: 600, fontSize: "var(--text-sm)", color: "var(--text-primary)" }}>{doc.title}</div>
{doc.description && (
<div style={{ fontSize: "var(--text-xs)", color: "var(--text-secondary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", maxWidth: 200 }}>
{doc.description}
</div>
)}
</div>
</div>
</td>
<td style={{ padding: "var(--space-3) var(--space-4)" }}>
<span style={{ fontSize: "var(--text-xs)", color: "var(--text-secondary)", textTransform: "uppercase", letterSpacing: "0.05em" }}>
{doc.mimeType.split("/").pop()?.toUpperCase()}
</span>
</td>
<td style={{ padding: "var(--space-3) var(--space-4)", fontSize: "var(--text-sm)", color: "var(--text-secondary)", fontVariantNumeric: "tabular-nums" }}>
{(doc.sizeBytes / 1024 / 1024).toFixed(2)} MB
</td>
<td style={{ padding: "var(--space-3) var(--space-4)" }}>
<StatusBadge status={doc.status as DocumentStatus} />
{doc.isForgeryFlagged && (
<span style={{ marginLeft: 6, fontSize: "var(--text-xs)", color: "var(--color-destructive)" }}>
<ShieldAlert size={12} /> Flagged
</span>
)}
</td>
<td style={{ padding: "var(--space-3) var(--space-4)" }}>
<Badge style={{ fontSize: "var(--text-xs)" }}>
{doc.versionCount ?? 1}
</Badge>
</td>
<td style={{ padding: "var(--space-3) var(--space-4)" }}>
<div style={{ display: "flex", gap: "var(--space-2)" }}>
<Button variant="ghost" size="xs" onClick={() => {}} title="Download">
<Download size={14} />
</Button>
<Button variant="ghost" size="xs" onClick={() => {}} title="Share">
<Share2 size={14} />
</Button>
<Button variant="ghost" size="xs" onClick={() => {}} title="Access Log">
<Clock size={14} />
</Button>
<Button
variant="destructive"
size="xs"
onClick={() => handleDelete(doc.id)}
disabled={removeDocument.isPending}
title="Delete"
>
<Trash2 size={14} />
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
</div>
);
}