Spaces:
Running
Running
Download apps/web/src/components/ui/documents/DocumentLibrary.tsx from Benaah/MicProtocol: direct link, hf CLI and curl.
- Browser
- Download file 12.7 kB
-
https://huggingface.co/spaces/Benaah/MicProtocol/resolve/main/apps/web/src/components/ui/documents/DocumentLibrary.tsx
- Command line
-
hf download hf://spaces/Benaah/MicProtocol/apps/web/src/components/ui/documents/DocumentLibrary.tsx
-
curl -L -o DocumentLibrary.tsx https://huggingface.co/spaces/Benaah/MicProtocol/resolve/main/apps/web/src/components/ui/documents/DocumentLibrary.tsx
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> | |
| ); | |
| } | |