import React, { useState, useCallback, useRef, useMemo, useEffect } from 'react'; import { 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 styles from './PageEditor.module.css'; import { useFileContext } from '../../contexts/FileContext'; import { FileId } from '../../types/file'; interface FileItem { id: FileId; name: string; pageCount: number; thumbnail: string | null; size: number; modifiedAt?: number | string | Date; } interface FileThumbnailProps { file: FileItem; index: number; totalFiles: number; selectedFiles: string[]; 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 FileThumbnail = ({ file, index, selectedFiles, onToggleFile, onDeleteFile, onSetStatus, onReorderFiles, onDownloadFile, isSupported = true, }: FileThumbnailProps) => { 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 downloadSelectedFile = useCallback(() => { // Prefer parent-provided handler if available if (typeof onDownloadFile === 'function') { onDownloadFile(file.id); return; } // Fallback: attempt to download using the File object if provided const maybeFile = (file as unknown as { file?: File }).file; if (maybeFile instanceof File) { const link = document.createElement('a'); link.href = URL.createObjectURL(maybeFile); link.download = maybeFile.name || file.name || 'download'; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(link.href); return; } // If we can't find a way to download, surface a status message onSetStatus?.(typeof t === 'function' ? t('downloadUnavailable', 'Download unavailable for this item') : 'Download unavailable for this item'); }, [file, onDownloadFile, onSetStatus, t]); const handleRef = useRef(null); // ---- Selection ---- const isSelected = selectedFiles.includes(file.id); // ---- 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()} >
)} {/* File content area */}
{/* Stacked file effect - multiple shadows to simulate pages */}
{file.thumbnail && ( {file.name} { // Hide broken image if blob URL was revoked const img = e.target as HTMLImageElement; img.style.display = 'none'; }} 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) */}
); }; export default React.memo(FileThumbnail);