import React, { useState, useCallback, useRef, useMemo, useEffect } from 'react'; import { Text, ActionIcon, CheckboxIndicator } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import MoreVertIcon from '@mui/icons-material/MoreVert'; import DownloadOutlinedIcon from '@mui/icons-material/DownloadOutlined'; import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; import PushPinIcon from '@mui/icons-material/PushPin'; import PushPinOutlinedIcon from '@mui/icons-material/PushPinOutlined'; import DragIndicatorIcon from '@mui/icons-material/DragIndicator'; import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; import { StirlingFileStub } from '../../types/fileContext'; import styles from './FileEditor.module.css'; import { useFileContext } from '../../contexts/FileContext'; import { FileId } from '../../types/file'; import { formatFileSize } from '../../utils/fileUtils'; import ToolChain from '../shared/ToolChain'; interface FileEditorThumbnailProps { file: StirlingFileStub; index: number; totalFiles: number; selectedFiles: FileId[]; selectionMode: boolean; onToggleFile: (fileId: FileId) => void; onDeleteFile: (fileId: FileId) => void; onViewFile: (fileId: FileId) => void; onSetStatus: (status: string) => void; onReorderFiles?: (sourceFileId: FileId, targetFileId: FileId, selectedFileIds: FileId[]) => void; onDownloadFile: (fileId: FileId) => void; toolMode?: boolean; isSupported?: boolean; } const FileEditorThumbnail = ({ file, index, selectedFiles, onToggleFile, onDeleteFile, onSetStatus, onReorderFiles, onDownloadFile, isSupported = true, }: FileEditorThumbnailProps) => { const { t } = useTranslation(); const { pinFile, unpinFile, isFilePinned, activeFiles } = useFileContext(); // ---- Drag state ---- const [isDragging, setIsDragging] = useState(false); const dragElementRef = useRef(null); const [actionsWidth, setActionsWidth] = useState(undefined); const [showActions, setShowActions] = useState(false); // Resolve the actual File object for pin/unpin operations const actualFile = useMemo(() => { return activeFiles.find(f => f.fileId === file.id); }, [activeFiles, file.id]); const isPinned = actualFile ? isFilePinned(actualFile) : false; const pageCount = file.processedFile?.totalPages || 0; const handleRef = useRef(null); // ---- Selection ---- const isSelected = selectedFiles.includes(file.id); // ---- Meta formatting ---- const prettySize = useMemo(() => { return formatFileSize(file.size); }, [file.size]); const extUpper = useMemo(() => { const m = /\.([a-z0-9]+)$/i.exec(file.name ?? ''); return (m?.[1] || '').toUpperCase(); }, [file.name]); const pageLabel = useMemo( () => pageCount > 0 ? `${pageCount} ${pageCount === 1 ? 'Page' : 'Pages'}` : '', [pageCount] ); const dateLabel = useMemo(() => { const d = new Date(file.lastModified); if (Number.isNaN(d.getTime())) return ''; return new Intl.DateTimeFormat(undefined, { month: 'short', day: '2-digit', year: 'numeric', }).format(d); }, [file.lastModified]); // ---- Drag & drop wiring ---- const fileElementRef = useCallback((element: HTMLDivElement | null) => { if (!element) return; dragElementRef.current = element; const dragCleanup = draggable({ element, getInitialData: () => ({ type: 'file', fileId: file.id, fileName: file.name, selectedFiles: [file.id] // Always drag only this file, ignore selection state }), onDragStart: () => { setIsDragging(true); }, onDrop: () => { setIsDragging(false); } }); const dropCleanup = dropTargetForElements({ element, getData: () => ({ type: 'file', fileId: file.id }), canDrop: ({ source }) => { const sourceData = source.data; return sourceData.type === 'file' && sourceData.fileId !== file.id; }, onDrop: ({ source }) => { const sourceData = source.data; if (sourceData.type === 'file' && onReorderFiles) { const sourceFileId = sourceData.fileId as FileId; const selectedFileIds = sourceData.selectedFiles as FileId[]; onReorderFiles(sourceFileId, file.id, selectedFileIds); } } }); return () => { dragCleanup(); dropCleanup(); }; }, [file.id, file.name, selectedFiles, onReorderFiles]); // Update dropdown width on resize useEffect(() => { const update = () => { if (dragElementRef.current) setActionsWidth(dragElementRef.current.offsetWidth); }; update(); window.addEventListener('resize', update); return () => window.removeEventListener('resize', update); }, []); // Close the actions dropdown when hovering outside this file card (and its dropdown) useEffect(() => { if (!showActions) return; const isInsideCard = (target: EventTarget | null) => { const container = dragElementRef.current; if (!container) return false; return target instanceof Node && container.contains(target); }; const handleMouseMove = (e: MouseEvent) => { if (!isInsideCard(e.target)) { setShowActions(false); } }; const handleTouchStart = (e: TouchEvent) => { // On touch devices, close if the touch target is outside the card if (!isInsideCard(e.target)) { setShowActions(false); } }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('touchstart', handleTouchStart, { passive: true }); return () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('touchstart', handleTouchStart); }; }, [showActions]); // ---- Card interactions ---- const handleCardClick = () => { if (!isSupported) return; onToggleFile(file.id); }; return (
{/* Header bar */}
{/* Logo/checkbox area */}
{isSupported ? ( onToggleFile(file.id)} color="var(--checkbox-checked-bg)" /> ) : (
{t('unsupported', 'Unsupported')}
)}
{/* Centered index */}
{index + 1}
{/* Kebab menu */} { e.stopPropagation(); setShowActions((v) => !v); }} >
{/* Actions overlay */} {showActions && (
e.stopPropagation()} >
)} {/* Title + meta line */}
{file.name} {/* e.g., v2 - Jan 29, 2025 - PDF file - 3 Pages */} {`v${file.versionNumber} - `} {dateLabel} {extUpper ? ` - ${extUpper} file` : ''} {pageLabel ? ` - ${pageLabel}` : ''}
{/* Preview area */}
{file.thumbnailUrl && ( {file.name} { const img = e.currentTarget; img.style.display = 'none'; img.parentElement?.setAttribute('data-thumb-missing', 'true'); }} style={{ maxWidth: '80%', maxHeight: '80%', objectFit: 'contain', borderRadius: 0, background: '#ffffff', border: '1px solid var(--border-default)', display: 'block', marginLeft: 'auto', marginRight: 'auto', alignSelf: 'start' }} /> )}
{/* Pin indicator (bottom-left) */} {isPinned && ( )} {/* Drag handle (span wrapper so we can attach a ref reliably) */} {/* Tool chain display at bottom */} {file.toolHistory && (
)}
); }; export default React.memo(FileEditorThumbnail);