/** * Card view for displaying document summary, preview, and metadata. */ import { useEffect, useRef, useState } from 'react'; import type { JSX } from 'react'; import { Download, FileText, Trash2 } from 'lucide-react'; import type { DmsDocument } from '../types'; import { downloadBlobFile, downloadDocumentContentMarkdown, downloadDocumentFile, getDocumentThumbnailBlob, } from '../lib/api'; /** * Defines properties accepted by the document card component. */ interface DocumentCardProps { document: DmsDocument; isSelected: boolean; isChecked: boolean; isTrashView: boolean; onSelect: (document: DmsDocument) => void; onToggleChecked: (documentId: string, checked: boolean) => void; onTrashDocument: (documentId: string) => Promise; onFilterPath: (path: string) => void; onFilterTag: (tag: string) => void; } /** * Defines visual processing status variants rendered in the card header indicator. */ type StatusTone = 'success' | 'progress' | 'failed'; /** * Resolves status tone and tooltip text from backend document status values. */ function statusPresentation(status: DmsDocument['status']): { tone: StatusTone; tooltip: string } { if (status === 'processed') { return { tone: 'success', tooltip: 'Processing status: success' }; } if (status === 'queued') { return { tone: 'progress', tooltip: 'Processing status: in progress' }; } if (status === 'error') { return { tone: 'failed', tooltip: 'Processing status: failed' }; } if (status === 'unsupported') { return { tone: 'failed', tooltip: 'Processing status: failed (unsupported type)' }; } return { tone: 'success', tooltip: 'Processing status: success (moved to trash)' }; } /** * Limits logical-path length while preserving start and end context with middle ellipsis. */ function compactLogicalPath(path: string, maxChars = 180): string { const normalized = path.trim(); if (!normalized) { return ''; } if (normalized.length <= maxChars) { return normalized; } const keepChars = Math.max(12, maxChars - 3); const headChars = Math.ceil(keepChars * 0.6); const tailChars = keepChars - headChars; return `${normalized.slice(0, headChars)}...${normalized.slice(-tailChars)}`; } /** * Renders one document card with optional image preview and searchable metadata. */ export default function DocumentCard({ document, isSelected, isChecked, isTrashView, onSelect, onToggleChecked, onTrashDocument, onFilterPath, onFilterTag, }: DocumentCardProps): JSX.Element { const [isTrashing, setIsTrashing] = useState(false); const [thumbnailObjectUrl, setThumbnailObjectUrl] = useState(null); const thumbnailObjectUrlRef = useRef(null); const createdDate = new Date(document.created_at).toLocaleString(); const status = statusPresentation(document.status); const compactPath = compactLogicalPath(document.logical_path, 180); const trashDisabled = isTrashView || document.status === 'trashed' || isTrashing; const trashTitle = trashDisabled ? 'Already in trash' : 'Move to trash'; /** * Loads thumbnail preview through authenticated fetch and revokes replaced object URLs. */ useEffect(() => { const revokeThumbnailObjectUrl = (): void => { if (!thumbnailObjectUrlRef.current) { return; } URL.revokeObjectURL(thumbnailObjectUrlRef.current); thumbnailObjectUrlRef.current = null; }; if (!document.preview_available) { revokeThumbnailObjectUrl(); setThumbnailObjectUrl(null); return; } let cancelled = false; const loadThumbnail = async (): Promise => { try { const blob = await getDocumentThumbnailBlob(document.id); if (cancelled) { return; } revokeThumbnailObjectUrl(); const objectUrl = URL.createObjectURL(blob); thumbnailObjectUrlRef.current = objectUrl; setThumbnailObjectUrl(objectUrl); } catch { if (cancelled) { return; } revokeThumbnailObjectUrl(); setThumbnailObjectUrl(null); } }; void loadThumbnail(); return () => { cancelled = true; revokeThumbnailObjectUrl(); }; }, [document.id, document.preview_available]); return (
onSelect(document)} onKeyDown={(event) => { if (event.currentTarget !== event.target) { return; } if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); onSelect(document); } }} >
{document.preview_available && thumbnailObjectUrl ? ( {document.original_filename} ) : (
{document.extension || 'file'}
)}

{compactPath}/ {document.original_filename}

{createdDate}

); }