diff --git a/frontend/src/components/fileManager/FileDetails.tsx b/frontend/src/components/fileManager/FileDetails.tsx index 9673d06ad..dcd460644 100644 --- a/frontend/src/components/fileManager/FileDetails.tsx +++ b/frontend/src/components/fileManager/FileDetails.tsx @@ -1,9 +1,9 @@ import React, { useState, useEffect } from 'react'; -import { Stack, Button } from '@mantine/core'; +import { Stack, Button, Box } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { useIndexedDBThumbnail } from '../../hooks/useIndexedDBThumbnail'; import { useFileManagerContext } from '../../contexts/FileManagerContext'; -import FilePreview from './FilePreview'; +import FilePreview from '../shared/FilePreview'; import FileInfoCard from './FileInfoCard'; import CompactFileDetails from './CompactFileDetails'; @@ -76,15 +76,18 @@ const FileDetails: React.FC = ({ return ( {/* Section 1: Thumbnail Preview */} - + + + {/* Section 2: File Details */} void; - onNext: () => void; -} - -const FilePreview: React.FC = ({ - currentFile, - thumbnail, - numberOfFiles, - isAnimating, - modalHeight, - onPrevious, - onNext -}) => { - const hasMultipleFiles = numberOfFiles > 1; - // Common style objects - const navigationArrowStyle = { - position: 'absolute' as const, - top: '50%', - transform: 'translateY(-50%)', - zIndex: 10 - }; - - const stackDocumentBaseStyle = { - position: 'absolute' as const, - width: '100%', - height: '100%' - }; - - const animationStyle = { - transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', - transform: isAnimating ? 'scale(0.95) translateX(1.25rem)' : 'scale(1) translateX(0)', - opacity: isAnimating ? 0.7 : 1 - }; - - const mainDocumentShadow = '0 6px 16px rgba(0, 0, 0, 0.2)'; - const stackDocumentShadows = { - back: '0 2px 8px rgba(0, 0, 0, 0.1)', - middle: '0 3px 10px rgba(0, 0, 0, 0.12)' - }; - - return ( - - - {/* Left Navigation Arrow */} - {hasMultipleFiles && ( - - - - )} - - {/* Document Stack Container */} - - {/* Background documents (stack effect) */} - {/* Show 2 shadow pages for 3+ files */} - {numberOfFiles >= 3 && ( - - )} - - {/* Show 1 shadow page for 2+ files */} - {numberOfFiles >= 2 && ( - - )} - - {/* Main document */} - {currentFile && thumbnail ? ( - {currentFile.name} - ) : currentFile ? ( -
- -
- ) : null} -
- - {/* Right Navigation Arrow */} - {hasMultipleFiles && ( - - - - )} -
-
- ); -}; - -export default FilePreview; \ No newline at end of file diff --git a/frontend/src/components/shared/FilePreview.tsx b/frontend/src/components/shared/FilePreview.tsx new file mode 100644 index 000000000..4a58d4671 --- /dev/null +++ b/frontend/src/components/shared/FilePreview.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { Box } from '@mantine/core'; +import { FileWithUrl } from '../../types/file'; +import DocumentThumbnail from './filePreview/DocumentThumbnail'; +import DocumentStack from './filePreview/DocumentStack'; +import HoverOverlay from './filePreview/HoverOverlay'; +import NavigationArrows from './filePreview/NavigationArrows'; + +export interface FilePreviewProps { + // Core file data + file: File | FileWithUrl | null; + thumbnail?: string | null; + + // Optional features + showStacking?: boolean; + showHoverOverlay?: boolean; + showNavigation?: boolean; + + // State + totalFiles?: number; + isAnimating?: boolean; + + // Event handlers + onFileClick?: (file: File | FileWithUrl | null) => void; + onPrevious?: () => void; + onNext?: () => void; +} + +const FilePreview: React.FC = ({ + file, + thumbnail, + showStacking = false, + showHoverOverlay = false, + showNavigation = false, + totalFiles = 1, + isAnimating = false, + onFileClick, + onPrevious, + onNext +}) => { + if (!file) return null; + + const hasMultipleFiles = totalFiles > 1; + + // Animation styles + const animationStyle = isAnimating ? { + transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', + transform: 'scale(0.95) translateX(1.25rem)', + opacity: 0.7 + } : {}; + + // Build the component composition + let content = ( + onFileClick?.(file)} + /> + ); + + // Wrap with hover overlay if needed + if (showHoverOverlay && onFileClick) { + content = {content}; + } + + // Wrap with document stack if needed + if (showStacking) { + content = ( + + {content} + + ); + } + + // Wrap with navigation if needed + if (showNavigation && hasMultipleFiles && onPrevious && onNext) { + content = ( + + {content} + + ); + } + + return ( + + {content} + + ); +}; + +export default FilePreview; \ No newline at end of file diff --git a/frontend/src/components/shared/filePreview/DocumentStack.tsx b/frontend/src/components/shared/filePreview/DocumentStack.tsx new file mode 100644 index 000000000..16168f6c9 --- /dev/null +++ b/frontend/src/components/shared/filePreview/DocumentStack.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { Box } from '@mantine/core'; + +export interface DocumentStackProps { + totalFiles: number; + children: React.ReactNode; +} + +const DocumentStack: React.FC = ({ + totalFiles, + children +}) => { + const stackDocumentBaseStyle = { + position: 'absolute' as const, + width: '100%', + height: '100%' + }; + + const stackDocumentShadows = { + back: '0 2px 8px rgba(0, 0, 0, 0.1)', + middle: '0 3px 10px rgba(0, 0, 0, 0.12)' + }; + + return ( + + {/* Background documents (stack effect) */} + {totalFiles >= 3 && ( + + )} + + {totalFiles >= 2 && ( + + )} + + {/* Main document container */} + + {children} + + + ); +}; + +export default DocumentStack; \ No newline at end of file diff --git a/frontend/src/components/shared/filePreview/DocumentThumbnail.tsx b/frontend/src/components/shared/filePreview/DocumentThumbnail.tsx new file mode 100644 index 000000000..661947be2 --- /dev/null +++ b/frontend/src/components/shared/filePreview/DocumentThumbnail.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { Box, Center, Image } from '@mantine/core'; +import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'; +import { FileWithUrl } from '../../../types/file'; + +export interface DocumentThumbnailProps { + file: File | FileWithUrl | null; + thumbnail?: string | null; + style?: React.CSSProperties; + onClick?: () => void; + children?: React.ReactNode; +} + +const DocumentThumbnail: React.FC = ({ + file, + thumbnail, + style = {}, + onClick, + children +}) => { + if (!file) return null; + + const containerStyle = { + position: 'relative' as const, + cursor: onClick ? 'pointer' : 'default', + transition: 'opacity 0.2s ease', + width: '100%', + height: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + ...style + }; + + if (thumbnail) { + return ( + + {`Preview + {children} + + ); + } + + return ( + +
+ +
+ {children} +
+ ); +}; + +export default DocumentThumbnail; \ No newline at end of file diff --git a/frontend/src/components/shared/filePreview/HoverOverlay.tsx b/frontend/src/components/shared/filePreview/HoverOverlay.tsx new file mode 100644 index 000000000..f09808982 --- /dev/null +++ b/frontend/src/components/shared/filePreview/HoverOverlay.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { Box } from '@mantine/core'; +import VisibilityIcon from '@mui/icons-material/Visibility'; + +export interface HoverOverlayProps { + onMouseEnter?: (e: React.MouseEvent) => void; + onMouseLeave?: (e: React.MouseEvent) => void; + children: React.ReactNode; +} + +const HoverOverlay: React.FC = ({ + onMouseEnter, + onMouseLeave, + children +}) => { + const defaultMouseEnter = (e: React.MouseEvent) => { + const overlay = e.currentTarget.querySelector('.hover-overlay') as HTMLElement; + if (overlay) overlay.style.opacity = '1'; + }; + + const defaultMouseLeave = (e: React.MouseEvent) => { + const overlay = e.currentTarget.querySelector('.hover-overlay') as HTMLElement; + if (overlay) overlay.style.opacity = '0'; + }; + + return ( + + {children} + + {/* Hover overlay */} + + + + + ); +}; + +export default HoverOverlay; \ No newline at end of file diff --git a/frontend/src/components/shared/filePreview/NavigationArrows.tsx b/frontend/src/components/shared/filePreview/NavigationArrows.tsx new file mode 100644 index 000000000..e9fc96719 --- /dev/null +++ b/frontend/src/components/shared/filePreview/NavigationArrows.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { Box, ActionIcon } from '@mantine/core'; +import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; +import ChevronRightIcon from '@mui/icons-material/ChevronRight'; + +export interface NavigationArrowsProps { + onPrevious: () => void; + onNext: () => void; + disabled?: boolean; + children: React.ReactNode; +} + +const NavigationArrows: React.FC = ({ + onPrevious, + onNext, + disabled = false, + children +}) => { + const navigationArrowStyle = { + position: 'absolute' as const, + top: '50%', + transform: 'translateY(-50%)', + zIndex: 10 + }; + + return ( + + {/* Left Navigation Arrow */} + + + + + {/* Content */} + + {children} + + + {/* Right Navigation Arrow */} + + + + + ); +}; + +export default NavigationArrows; \ No newline at end of file diff --git a/frontend/src/components/tools/shared/FileMetadata.tsx b/frontend/src/components/tools/shared/FileMetadata.tsx new file mode 100644 index 000000000..00e33f175 --- /dev/null +++ b/frontend/src/components/tools/shared/FileMetadata.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { Stack, Text } from '@mantine/core'; +import { formatFileSize, getFileDate } from '../../../utils/fileUtils'; + +export interface FileMetadataProps { + file: File; +} + +const FileMetadata = ({ file }: FileMetadataProps) => { + return ( + + + + {formatFileSize(file.size)} + + + {file.type || 'Unknown'} + + + {getFileDate(file)} + + + + ); +}; + +export default FileMetadata; \ No newline at end of file diff --git a/frontend/src/components/tools/shared/NavigationControls.tsx b/frontend/src/components/tools/shared/NavigationControls.tsx new file mode 100644 index 000000000..2b5306778 --- /dev/null +++ b/frontend/src/components/tools/shared/NavigationControls.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { Stack, Group, ActionIcon, Box, Text } from '@mantine/core'; +import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; +import ChevronRightIcon from '@mui/icons-material/ChevronRight'; + +export interface NavigationControlsProps { + currentIndex: number; + totalFiles: number; + onPrevious: () => void; + onNext: () => void; + onIndexChange: (index: number) => void; +} + +const NavigationControls = ({ + currentIndex, + totalFiles, + onPrevious, + onNext, + onIndexChange +}: NavigationControlsProps) => { + if (totalFiles <= 1) return null; + + return ( + + + + + + + + {Array.from({ length: totalFiles }, (_, index) => ( + onIndexChange(index)} + data-testid={`review-panel-dot-${index}`} + /> + ))} + + + + + + + + + {currentIndex + 1} of {totalFiles} + + + ); +}; + +export default NavigationControls; \ No newline at end of file diff --git a/frontend/src/components/tools/shared/ReviewPanel.tsx b/frontend/src/components/tools/shared/ReviewPanel.tsx index 451ef4e9b..31b0c9b76 100644 --- a/frontend/src/components/tools/shared/ReviewPanel.tsx +++ b/frontend/src/components/tools/shared/ReviewPanel.tsx @@ -1,8 +1,8 @@ import React, { useState } from 'react'; -import { Paper, Box, Image, Text, Loader, Stack, Center, Group, ActionIcon, Flex } from '@mantine/core'; -import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; -import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import VisibilityIcon from '@mui/icons-material/Visibility'; +import { Box, Text, Loader, Stack, Center, Flex } from '@mantine/core'; +import FilePreview from '../../shared/FilePreview'; +import FileMetadata from './FileMetadata'; +import NavigationControls from './NavigationControls'; export interface ReviewFile { file: File; @@ -28,22 +28,6 @@ const ReviewPanel = ({ }: ReviewPanelProps) => { const [currentIndex, setCurrentIndex] = useState(0); - const formatSize = (size: number) => { - if (size > 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(1)} MB`; - if (size > 1024) return `${(size / 1024).toFixed(1)} KB`; - return `${size} B`; - }; - - const formatDate = (date: Date) => { - return new Intl.DateTimeFormat('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - }).format(date); - }; - const handlePrevious = () => { setCurrentIndex((prev) => (prev === 0 ? files.length - 1 : prev - 1)); }; @@ -75,7 +59,7 @@ const ReviewPanel = ({ if (!currentFile) return null; return ( - + {/* File name at the top */} @@ -92,152 +76,26 @@ const ReviewPanel = ({ - - {/* Preview on the left */} - - onFileClick?.(currentFile.file)} - onMouseEnter={(e) => { - if (onFileClick) { - const overlay = e.currentTarget.querySelector('.hover-overlay'); - if (overlay) overlay.style.opacity = '1'; - } - }} - onMouseLeave={(e) => { - const overlay = e.currentTarget.querySelector('.hover-overlay'); - if (overlay) overlay.style.opacity = '0'; - }} - > - {currentFile.thumbnail ? ( - {`Preview - ) : ( - - No preview - - )} - - {/* Hover overlay with eye icon */} - {onFileClick && ( - - - - )} - + + + file && onFileClick(file as File) : undefined} + /> - - {/* Metadata on the right */} - - - - {formatSize(currentFile.file.size)} - - - {currentFile.file.type || 'Unknown'} - - - {formatDate(new Date(currentFile.file.lastModified))} - - - + {/* Navigation controls */} - {files.length > 1 && ( - - - - - - - - {files.map((_, index) => ( - setCurrentIndex(index)} - data-testid={`review-panel-dot-${index}`} - /> - ))} - - - - - - - - - {currentIndex + 1} of {files.length} - - - )} + ); };