diff --git a/src/components/Dateien/DateienTable.tsx b/src/components/Dateien/DateienTable.tsx index c820a8c..af00ffe 100644 --- a/src/components/Dateien/DateienTable.tsx +++ b/src/components/Dateien/DateienTable.tsx @@ -1,6 +1,7 @@ import { FormGenerator } from '../FormGenerator'; import { useLanguage } from '../../contexts/LanguageContext'; import { Popup, EditForm } from '../Popup'; +import { FilePreview } from '../FilePreview'; import styles from './DateienTable.module.css'; import { useDateienLogic } from './dateienLogic.tsx'; import type { DateienTableProps } from './dateienInterfaces'; @@ -18,9 +19,11 @@ export function DateienTable({ className = '' }: DateienTableProps) { editModalOpen, editingFile, editFileFields, + previewModalOpen, + previewingFile, handleSaveFile, handleCancelEdit, - refetch, + handleClosePreview, handleDelete, handleDeleteMultiple } = useDateienLogic(); @@ -76,6 +79,17 @@ export function DateienTable({ className = '' }: DateienTableProps) { /> )} + + {/* File Preview Modal */} + {previewingFile && ( + + )} ); } diff --git a/src/components/Dateien/dateienInterfaces.ts b/src/components/Dateien/dateienInterfaces.ts index d588e97..8a32a2e 100644 --- a/src/components/Dateien/dateienInterfaces.ts +++ b/src/components/Dateien/dateienInterfaces.ts @@ -77,14 +77,20 @@ export interface DateienLogicReturn { actions: TableAction[]; downloadingFiles: Set; deletingFiles: Set; + previewingFiles: Set; downloadError: string | null; deleteError: string | null; + previewError: string | null; editModalOpen: boolean; editingFile: UserFile | null; editFileFields: EditFieldConfig[]; + previewModalOpen: boolean; + previewingFile: UserFile | null; handleEditFile: (file: UserFile) => void; handleSaveFile: (updatedFile: UserFile) => Promise; handleCancelEdit: () => void; + handlePreviewFile: (file: UserFile) => void; + handleClosePreview: () => void; handleDelete: (file: UserFile) => Promise; handleDeleteMultiple: (files: UserFile[]) => Promise; } diff --git a/src/components/Dateien/dateienLogic.tsx b/src/components/Dateien/dateienLogic.tsx index bebf2bb..f050222 100644 --- a/src/components/Dateien/dateienLogic.tsx +++ b/src/components/Dateien/dateienLogic.tsx @@ -1,5 +1,5 @@ import { useMemo, useState } from 'react'; -import { IoIosTrash, IoIosDownload } from 'react-icons/io'; +import { IoIosTrash, IoIosDownload, IoIosEye } from 'react-icons/io'; import { MdModeEdit } from 'react-icons/md'; import { ColumnConfig } from '../FormGenerator'; @@ -22,8 +22,10 @@ export function useDateienLogic(): DateienLogicReturn { handleFileUpdate, downloadingFiles, deletingFiles, + previewingFiles, downloadError, - deleteError + deleteError, + previewError } = useFileOperations(); const { t } = useLanguage(); @@ -31,6 +33,10 @@ export function useDateienLogic(): DateienLogicReturn { const [editModalOpen, setEditModalOpen] = useState(false); const [editingFile, setEditingFile] = useState(null); + // Preview modal state + const [previewModalOpen, setPreviewModalOpen] = useState(false); + const [previewingFile, setPreviewingFile] = useState(null); + // Configure edit fields for filename editing const editFileFields: EditFieldConfig[] = useMemo(() => [ { @@ -90,6 +96,18 @@ export function useDateienLogic(): DateienLogicReturn { setEditingFile(null); }; + // Handle preview file + const handlePreviewFile = (file: UserFile) => { + setPreviewingFile(file); + setPreviewModalOpen(true); + }; + + // Handle close preview + const handleClosePreview = () => { + setPreviewModalOpen(false); + setPreviewingFile(null); + }; + // Helper function to format file size const formatFileSize: FileSizeFormatter = (sizeInBytes) => { if (!sizeInBytes || sizeInBytes === 0) return '-'; @@ -332,6 +350,19 @@ export function useDateienLogic(): DateienLogicReturn { // Configure action buttons const actions: TableAction[] = useMemo(() => [ + { + label: t('files.action.preview', 'Preview'), + icon: (row: UserFile) => { + const isPreviewingThis = previewingFiles.has(row.id); + if (isPreviewingThis) return '⏳'; + return ; + }, + onClick: (row: UserFile) => { + if (!previewingFiles.has(row.id)) { + handlePreviewFile(row); + } + } + }, { label: t('files.action.edit', 'Edit'), icon: , @@ -365,7 +396,7 @@ export function useDateienLogic(): DateienLogicReturn { } } } - ], [t, downloadingFiles, deletingFiles, handleDownload, handleDelete]); + ], [t, previewingFiles, downloadingFiles, deletingFiles, handleDownload, handleDelete]); return { files, @@ -376,14 +407,20 @@ export function useDateienLogic(): DateienLogicReturn { actions, downloadingFiles, deletingFiles, + previewingFiles, downloadError, deleteError, + previewError, editModalOpen, editingFile, editFileFields, + previewModalOpen, + previewingFile, handleEditFile, handleSaveFile, handleCancelEdit, + handlePreviewFile, + handleClosePreview, handleDelete, handleDeleteMultiple }; diff --git a/src/components/FilePreview/FilePreview.module.css b/src/components/FilePreview/FilePreview.module.css new file mode 100644 index 0000000..085ef17 --- /dev/null +++ b/src/components/FilePreview/FilePreview.module.css @@ -0,0 +1,752 @@ + +.previewContainer { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: var(--color-background)!important; + border-radius: 8px; + overflow: hidden; + position: relative; +} + +/* Ensure all child elements have white background */ +.previewContainer * { + background-color: var(--color-background) !important; +} + +/* Loading State */ +.loadingContainer { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1rem; + padding: 2rem; + color: var(--color-text); +} + +.spinner { + width: 40px; + height: 40px; + border: 4px solid var(--color-primary); + border-top: 4px solid var(--color-primary); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Error State */ +.errorContainer { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1rem; + padding: 2rem; + color: var(--color-error); + text-align: center; +} + +.errorIcon { + font-size: 3rem; + margin-bottom: 0.5rem; +} + +.retryButton { + background: var(--color-primary); + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: 6px; + cursor: pointer; + font-size: 1rem; + transition: background-color 0.2s ease; +} + +.retryButton:hover { + background: var(--color-primary-hover); +} + +/* Image Preview */ +.previewImage { + max-width: 100%; + max-height: 100%; + object-fit: contain; + border-radius: 4px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +/* Iframe Preview (for PDFs, text files, etc.) */ +.previewIframe { + width: 100%; + height: 100%; + border: none; + border-radius: 4px; + background: var(--color-background) !important; + color: var(--color-text) !important; +} + +/* Force iframe content to have white background */ +.previewIframe::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: var(--color-background) !important; + z-index: -1; +} + +/* Unsupported File Type */ +.unsupportedContainer { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1rem; + padding: 2rem; + color: var(--color-text); + text-align: center; +} + +.unsupportedIcon { + font-size: 4rem; + margin-bottom: 1rem; + opacity: 0.6; +} + +.fileName { + font-weight: 500; + font-size: 1.1rem; + color: var(--color-text); + margin: 0.5rem 0; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .previewContainer { + padding: 1rem; + } + + .loadingContainer, + .errorContainer, + .unsupportedContainer { + padding: 1rem; + } + + .previewImage { + max-height: 70vh; + } + + .previewIframe { + height: 70vh; + } +} + + +/* JSON Container */ +.jsonContainer { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + background: var(--color-background) !important; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +/* JSON Header */ +.jsonHeader { + background: var(--color-background) !important; + border-bottom: 1px solid #e9ecef; + padding: 12px 20px; + display: flex; + justify-content: space-between; + align-items: center; + font-size: 14px; +} + +.jsonHeaderRight { + display: flex; + align-items: center; + gap: 12px; +} + +.jsonTitle { + font-weight: 600; + color: #495057; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} + +.jsonSize { + color: var(--color-text); + font-size: 12px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} + + + +/* Table Layout - Row-wise rendering */ +.jsonTable { + flex: 1; + overflow: hidden; + background: var(--color-background) !important; + display: flex; + flex-direction: column; +} + +/* Collapsible functionality */ +.collapseButton { + background: none; + border: none; + cursor: pointer; + padding: 2px 6px; + margin-left: 8px; + font-size: 12px; + color: var(--color-gray); + border-radius: 3px; + transition: all 0.2s; + font-weight: bold; + min-width: 20px; + text-align: center; +} + +.collapseButton:hover { + background-color: var(--color-gray-hover); + color: #333; +} + +.collapseButton:active { + transform: scale(0.95); +} + +.valueContainer { + display: flex; + flex-direction: column; + width: 100%; + overflow: visible !important; + min-width: 0; +} + +.jsonValuePreview { + color: #666; + font-style: italic; + background: var(--color-background); + padding: 4px 8px; + border-radius: 4px; + border-left: 3px solid var(--color-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; + display: block; + font-size: 12px; +} + +.jsonValue { + word-wrap: break-word; + white-space: pre-wrap; + max-width: 100%; + display: block; + position: relative; + z-index: 10; + min-height: 18px; +} + +.collapsedRow { + background-color: #f8f9fa; + opacity: 0.8; +} + +.collapsedRow .jsonTableKey { + border-left: 2px solid var(--color-secondary); +} + +.notCollapsedRow { + background-color: #f8f9fa; + border-left: 2px solid var(--color-background); +} + +.collapsedRow:hover { + opacity: 1; +} + +.jsonTableBody { + flex: 1; + overflow-y: auto; + background: var(--color-background) !important; +} + +.jsonTableRow { + display: flex; + border-bottom: 1px solid var(--color-primary); + transition: background-color 0.2s ease; +} + +.jsonTableRow:hover { + background: var(--color-background); +} + +.jsonTableKey { + flex: 0 0 200px; + padding: 12px 16px; + border-right: 1px solid var(--color-primary); + border-left: 2px solid var(--color-background); + background: var(--color-background); + display: flex; + align-items: flex-start; + box-sizing: border-box; +} + +.jsonTableValue { + flex: 1; + padding: 12px 16px; + background: var(--color-background) !important; + display: flex; + align-items: flex-start; + box-sizing: border-box; +} + +.jsonKey { + font-family: 'Fira Code', 'Monaco', 'Cascadia Code', 'Roboto Mono', 'Courier New', monospace; + font-size: 13px; + font-weight: 600; + color: var(--color-text); + word-break: break-all; + background: transparent; + line-height: 1.4; + width: 100%; +} + +.jsonValue { + font-family: 'Fira Code', 'Monaco', 'Cascadia Code', 'Roboto Mono', 'Courier New', monospace; + font-size: 13px; + color: var(--color-text); + word-break: break-word; + white-space: pre-wrap; + background: transparent; + line-height: 1.4; + width: 100%; + position: relative; + z-index: 10; + min-height: 18px; +} + +/* Type-specific styling */ +.jsonValueString { + color: var(--color-text); + font-weight: 600; + font-family: 'Fira Code', 'Monaco', 'Cascadia Code', 'Roboto Mono', 'Courier New', monospace; + font-size: 13px; + white-space: nowrap !important; +} + +.jsonValueNumber { + color: var(--color-text); + font-weight: 600; + white-space: nowrap !important; + word-break: keep-all !important; + overflow: visible !important; +} + +.jsonValueBoolean { + color: var(--color-text); + font-weight: 600; + white-space: nowrap !important; + word-break: keep-all !important; + overflow: visible !important; +} + +.jsonValueNull { + color: var(--color-text); + font-style: italic; +} + +.jsonValueUndefined { + color: var(--color-text); + font-style: italic; +} + +.jsonValueArray { + color: #fd7e14; + font-weight: 600; +} + +.jsonValueObject { + color: #6f42c1; + font-weight: 600; +} + +.jsonValueTimestamp { + color: var(--color-text); + font-weight: 600; + font-family: 'Fira Code', 'Monaco', 'Cascadia Code', 'Roboto Mono', 'Courier New', monospace; +} + +/* Dark mode support for JSON table layout */ +[data-theme="dark"] .jsonTableHeader { + background: #2d3748; + border-bottom-color: #4a5568; + color: #e2e8f0; +} + +[data-theme="dark"] .jsonTableKeyHeader { + background: #2d3748; + border-right-color: #4a5568; +} + +[data-theme="dark"] .jsonTableValueHeader { + background: #2d3748; +} + +[data-theme="dark"] .jsonTableBody { + background: #1a202c !important; +} + +[data-theme="dark"] .jsonTableRow { + border-bottom-color: #2d3748; +} + +[data-theme="dark"] .jsonTableRow:hover { + background: #2d3748; +} + +[data-theme="dark"] .jsonTableKey { + background: #2d3748; + border-right-color: #4a5568; +} + +[data-theme="dark"] .jsonTableValue { + background: #1a202c !important; +} + +[data-theme="dark"] .jsonKey { + color: #e2e8f0; +} + +[data-theme="dark"] .jsonValue { + color: #e2e8f0; +} + +[data-theme="dark"] .jsonValueString { + color: #63b3ed; +} + +[data-theme="dark"] .jsonValueNumber { + color: #68d391; +} + +[data-theme="dark"] .jsonValueBoolean { + color: #fc8181; +} + +[data-theme="dark"] .jsonValueNull, +[data-theme="dark"] .jsonValueUndefined { + color: #a0aec0; +} + +[data-theme="dark"] .jsonValueArray { + color: #f6ad55; +} + +[data-theme="dark"] .jsonValueObject { + color: #b794f6; +} + +[data-theme="dark"] .jsonValueTimestamp { + color: #4fd1c7; +} + +/* Dark mode for collapsible functionality */ +[data-theme="dark"] .collapseButton { + color: #a0aec0; +} + +[data-theme="dark"] .collapseButton:hover { + background-color: #4a5568; +} + +[data-theme="dark"] .jsonValuePreview { + color: #a0aec0; + background: #2d3748; + border-left-color: #4a5568; +} + +[data-theme="dark"] .collapsedRow { + background-color: #2d3748; +} + +[data-theme="dark"] .collapsedRow .jsonTableKey { + border-left-color: #63b3ed; +} + +[data-theme="dark"] .collapsedRow:hover { + background-color: #4a5568; +} + +/* Nested Table Styles */ +.nestedTable { + margin-top: 8px; + border-radius: 4px; + background: #f8f9fa; + overflow: visible !important; + width: 100%; + min-width: 100%; +} + + + + +.nestedTableKeyHeader { + flex: 0 0 150px; + padding: 8px 12px; + border-right: 1px solid #dee2e6; + background: #e9ecef; +} + +.nestedTableValueHeader { + flex: 1; + padding: 8px 12px; + background: #e9ecef; +} + +.nestedTableBody { + background: var(--color-background) !important; +} + +.nestedTableRow { + display: flex; + transition: background-color 0.2s ease; + overflow: visible !important; + min-height: 30px; +} + +.nestedTableRow:hover { + background: var(--color-background); +} + +.nestedTableRow:last-child { + border-bottom: none; +} + +.nestedTableKey { + flex: 0 0 150px; + padding: 8px 12px; + background: var(--color-background); + display: flex; + align-items: flex-start; + box-sizing: border-box; +} + +.nestedTableValue { + flex: 1; + padding: 8px 12px; + background: var(--color-background) !important; + display: flex; + align-items: flex-start; + box-sizing: border-box; + position: relative; + z-index: 1; + overflow: visible !important; + min-width: 0; + width: 100%; +} + +.nestedValueSummary { + margin-bottom: 8px; + font-weight: 500; + color: var(--color-text); +} + +/* Array items display */ +.arrayItems { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 4px; +} + +/* Array items when no key is shown (should span full width) */ +.arrayItemsFullWidth { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 4px; + margin-left: -183px; + padding-left: 16px; + width: calc(100% + 183px); +} + +.arrayItem { + display: flex; + align-items: center; + padding: 2px 6px; + background: #f8f9fa; + border-radius: 3px; + font-size: 12px; +} + +.arrayValue { + color: var(--color-text); + font-weight: 400; +} + +.arrayPreview { + color: var(--color-light-gray); + font-size: 12px; + font-style: italic; + padding: 4px 8px; + background: var(--color-background); + border-radius: 3px; + border: 1px solid green; +} + +/* JSON Syntax Highlighting */ +.jsonCode { + color: #212529; + background: white !important; +} + +/* Key highlighting */ +.jsonCode { + background: linear-gradient(90deg, + transparent 0%, + transparent 100% + ); +} + +/* Add some basic syntax highlighting using CSS */ +.jsonCode::before { + content: ''; + display: block; + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + pointer-events: none; + background: + /* Strings - green */ + linear-gradient(90deg, transparent 0%, transparent 100%), + /* Numbers - blue */ + linear-gradient(90deg, transparent 0%, transparent 100%), + /* Booleans - purple */ + linear-gradient(90deg, transparent 0%, transparent 100%), + /* Null - gray */ + linear-gradient(90deg, transparent 0%, transparent 100%); +} + +/* Scrollbar styling */ +.jsonPreview::-webkit-scrollbar { + width: 8px; + height: 8px; + background: var(--color-background) !important; +} + +.jsonPreview::-webkit-scrollbar-track { + background: var(--color-background) !important; + border-radius: 4px; +} + +.jsonPreview::-webkit-scrollbar-thumb { + background: #c1c1c1 !important; + border-radius: 4px; +} + +.jsonPreview::-webkit-scrollbar-thumb:hover { + background: #a8a8a8 !important; +} + +/* JSON Structure Indicators */ +.jsonPreview { + position: relative; + background: var(--color-background) !important; +} + +/* Add line numbers */ +.jsonPreview::before { + content: ''; + position: absolute; + left: 0; + top: 0; + width: 40px; + height: 100%; + background: var(--color-background) !important; + border-right: 1px solid #e9ecef; + z-index: 1; +} + +/* Dark mode adjustments */ +@media (prefers-color-scheme: dark) { + .previewIframe { + background: white !important; + color: black !important; + } + + /* Only apply dark background for non-HTML content */ + .previewIframe[data-mime-type*="application/pdf"] { + background: #1a1a1a !important; + } + + /* Keep JSON files with light background for readability */ + .previewIframe[data-mime-type*="application/json"] { + background: var(--color-background) !important; + color: black !important; + } + + .jsonPreview { + background: var(--color-background) !important; + color: black !important; + } + + /* Dark mode for JSON container */ + .jsonContainer { + background: #1e1e1e; + color: #d4d4d4; + } + + .jsonHeader { + background: #2d2d30; + border-bottom-color: #3e3e42; + } + + .jsonTitle { + color: #cccccc; + } + + .jsonSize { + color: #969696; + } + + .jsonPreview { + background: #1e1e1e !important; + color: #d4d4d4 !important; + } + + .jsonPreview::before { + background: #2d2d30; + border-right-color: #3e3e42; + } + + .jsonPreview::-webkit-scrollbar-track { + background: #2d2d30; + } + + .jsonPreview::-webkit-scrollbar-thumb { + background: #555; + } + + .jsonPreview::-webkit-scrollbar-thumb:hover { + background: #777; + } +} + diff --git a/src/components/FilePreview/FilePreview.tsx b/src/components/FilePreview/FilePreview.tsx new file mode 100644 index 0000000..f9ec3c3 --- /dev/null +++ b/src/components/FilePreview/FilePreview.tsx @@ -0,0 +1,246 @@ +import { useState, useEffect } from 'react'; + +import { IoIosDownload, IoIosCopy } from 'react-icons/io'; + + +import { Popup, PopupAction } from '../Popup/Popup'; +import { useLanguage } from '../../contexts/LanguageContext'; +import { useFileOperations } from '../../hooks/useFiles'; +import { + JsonRenderer, + ImageRenderer, + TextRenderer, + PdfRenderer, + ApplicationRenderer, + UnsupportedRenderer, + LoadingRenderer, + ErrorRenderer +} from './renderers'; +import styles from './FilePreview.module.css'; + +export interface FilePreviewProps { + isOpen: boolean; + onClose: () => void; + fileId: string; + fileName: string; + mimeType?: string; +} + +export function FilePreview({ + isOpen, + onClose, + fileId, + fileName, + mimeType +}: FilePreviewProps) { + const { t } = useLanguage(); + const { handleFilePreview, handleFileDownload, previewingFiles, previewError, downloadingFiles } = useFileOperations(); + const [previewUrl, setPreviewUrl] = useState(null); + const [previewContent, setPreviewContent] = useState(null); + const [error, setError] = useState(null); + const [copySuccess, setCopySuccess] = useState(false); + + // Clean up blob URL when component unmounts or preview changes + useEffect(() => { + return () => { + if (previewUrl) { + window.URL.revokeObjectURL(previewUrl); + } + }; + }, [previewUrl]); + + // Load preview when modal opens + useEffect(() => { + if (isOpen && fileId) { + loadPreview(); + } else { + // Clean up when modal closes + if (previewUrl) { + window.URL.revokeObjectURL(previewUrl); + setPreviewUrl(null); + } + setError(null); + } + }, [isOpen, fileId]); + + + const loadPreview = async () => { + try { + setError(null); + setPreviewContent(null); + const result = await handleFilePreview(fileId, fileName); + + if (result.success && result.previewUrl) { + setPreviewUrl(result.previewUrl); + if (result.decodedContent) { + setPreviewContent(result.decodedContent); + } + } else { + setError(result.error || 'Failed to load preview'); + } + } catch (err) { + setError('An unexpected error occurred while loading the preview'); + } + }; + + const handleCopyContent = async () => { + try { + if (previewContent) { + await navigator.clipboard.writeText(previewContent); + setCopySuccess(true); + setTimeout(() => setCopySuccess(false), 2000); + } else { + // Fallback: try to copy from preview URL if it's a text file + if (previewUrl && mimeType?.startsWith('text/')) { + const response = await fetch(previewUrl); + const text = await response.text(); + await navigator.clipboard.writeText(text); + setCopySuccess(true); + setTimeout(() => setCopySuccess(false), 2000); + } + } + } catch (err) { + console.error('Failed to copy content:', err); + } + }; + + const handleDownloadFile = async () => { + try { + await handleFileDownload(fileId, fileName); + } catch (err) { + console.error('Failed to download file:', err); + } + }; + + const isPreviewing = previewingFiles.has(fileId); + const hasError = error || previewError; + + // Create action buttons for the popup header + const actions: PopupAction[] = [ + // Copy Content Button - only show for text-based files (exclude PDFs) + ...(mimeType !== 'application/pdf' && (mimeType?.startsWith('text/') || mimeType === 'application/json' || previewContent) ? [{ + label: copySuccess ? t('files.preview.copied', 'Copied!') : t(''), + icon: copySuccess ? '✓' : , + onClick: handleCopyContent, + disabled: !previewContent && !previewUrl, + variant: 'primary' as const + }] : []), + + // Download Button + { + label: String(''), + icon: downloadingFiles.has(fileId) ? undefined : , + onClick: handleDownloadFile, + disabled: downloadingFiles.has(fileId), + loading: downloadingFiles.has(fileId), + variant: 'success' as const + } + ]; + + const renderPreview = () => { + if (!previewUrl) { + if (isPreviewing) { + return ; + } + + if (hasError) { + return ; + } + + return null; + } + + // For JSON files with decoded content, use JsonRenderer + if (previewContent && mimeType === 'application/json') { + return ; + } + + if (mimeType === 'application/json') { + return ( +
+
+ JSON Preview (Fallback) +
+ Raw content +
+
+
+            
+              {previewContent || 'No content available'}
+            
+          
+
+ ); + } + + // Determine preview type based on MIME type + const mimePrefix = mimeType?.split('/')[0]; + + + + switch (mimePrefix) { + case 'image': + return ( + setError('Failed to load image preview')} + /> + ); + + case 'text': + return ( + setError('Failed to load text preview')} + /> + ); + + case 'application': + if (mimeType === 'application/pdf') { + return ( + setError('Failed to load PDF preview')} + /> + ); + } + + + + return ( + setError('Preview not supported for this file type')} + /> + ); + + default: + return ; + } + }; + + return ( + +
+ {renderPreview()} +
+
+ ); +} + +export default FilePreview; + + diff --git a/src/components/FilePreview/index.ts b/src/components/FilePreview/index.ts new file mode 100644 index 0000000..aa286d3 --- /dev/null +++ b/src/components/FilePreview/index.ts @@ -0,0 +1,2 @@ +export { FilePreview } from './FilePreview'; +export type { FilePreviewProps } from './FilePreview'; diff --git a/src/components/FilePreview/renderers/ApplicationRenderer.tsx b/src/components/FilePreview/renderers/ApplicationRenderer.tsx new file mode 100644 index 0000000..f3e3c60 --- /dev/null +++ b/src/components/FilePreview/renderers/ApplicationRenderer.tsx @@ -0,0 +1,21 @@ +import styles from '../FilePreview.module.css'; + +interface ApplicationRendererProps { + previewUrl: string; + fileName: string; + mimeType?: string; + onError: () => void; +} + +export function ApplicationRenderer({ previewUrl, fileName, mimeType, onError }: ApplicationRendererProps) { + return ( +