From ade78077543f30f70b903f58834877b6a9161125 Mon Sep 17 00:00:00 2001 From: noah Date: Mon, 9 Mar 2026 19:10:41 +0100 Subject: [PATCH] feat: add file preview modal + harden PDF conversion - Add PreviewModal component with support for images, PDFs, HTML, audio, video, and text-based formats - FileCard now shows Preview + Download buttons side-by-side after conversion - Preview opens in a modal overlay with Escape/backdrop-click to close - Text files truncated at 100KB for preview performance - Harden pdfjs-dist worker loading with try/catch fallback (runs on main thread if CDN fails) - Add useSystemFonts option to PDF document loading for better text extraction --- src/app/convert/page.tsx | 15 ++ src/components/FileCard.tsx | 26 ++- src/components/PreviewModal.tsx | 277 ++++++++++++++++++++++++ src/lib/converters/documentConverter.ts | 14 +- 4 files changed, 324 insertions(+), 8 deletions(-) create mode 100644 src/components/PreviewModal.tsx diff --git a/src/app/convert/page.tsx b/src/app/convert/page.tsx index 625de5b..67670dd 100644 --- a/src/app/convert/page.tsx +++ b/src/app/convert/page.tsx @@ -1,12 +1,15 @@ 'use client'; +import { useState } from 'react'; import { AnimatePresence, motion } from 'framer-motion'; import Link from 'next/link'; import { DropZone } from '@/components/DropZone'; import { FileCard } from '@/components/FileCard'; +import { PreviewModal } from '@/components/PreviewModal'; import { useFileUpload } from '@/hooks/useFileUpload'; import { useConversion } from '@/hooks/useConversion'; import { formatFileSize } from '@/lib/utils'; +import { UploadedFile } from '@/types'; export default function ConvertPage() { const { @@ -32,6 +35,8 @@ export default function ConvertPage() { downloadAllAsZip, } = useConversion(updateFile); + const [previewFile, setPreviewFile] = useState(null); + const hasFiles = files.length > 0; const convertableCount = files.filter( (f) => f.targetFormat && f.status !== 'done' && f.availableFormats.length > 0 @@ -86,6 +91,7 @@ export default function ConvertPage() { onSetFormat={setTargetFormat} onRemove={removeFile} onDownload={downloadFile} + onPreview={setPreviewFile} /> ))} @@ -164,6 +170,15 @@ export default function ConvertPage() { )} + {/* Preview Modal */} + setPreviewFile(null)} + onDownload={(f) => { + downloadFile(f); + setPreviewFile(null); + }} + /> ); } diff --git a/src/components/FileCard.tsx b/src/components/FileCard.tsx index c8044e7..229ddd9 100644 --- a/src/components/FileCard.tsx +++ b/src/components/FileCard.tsx @@ -11,6 +11,7 @@ interface FileCardProps { onSetFormat: (id: string, format: string) => void; onRemove: (id: string) => void; onDownload: (file: UploadedFile) => void; + onPreview: (file: UploadedFile) => void; } export function FileCard({ @@ -19,6 +20,7 @@ export function FileCard({ onSetFormat, onRemove, onDownload, + onPreview, }: FileCardProps) { const categoryColor = CATEGORY_COLORS[file.category]; const categoryLabel = CATEGORY_LABELS[file.category]; @@ -168,21 +170,35 @@ export function FileCard({ )} - {/* Download button */} + {/* Action buttons */} {file.status === 'done' && ( -
+
onPreview(file)} + initial={{ opacity: 0, y: 5 }} + animate={{ opacity: 1, y: 0 }} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > + + + + + Preview + + onDownload(file)} initial={{ opacity: 0, y: 5 }} animate={{ opacity: 1, y: 0 }} whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} > - + - Download .{file.targetFormat} + .{file.targetFormat}
)} diff --git a/src/components/PreviewModal.tsx b/src/components/PreviewModal.tsx new file mode 100644 index 0000000..17a793b --- /dev/null +++ b/src/components/PreviewModal.tsx @@ -0,0 +1,277 @@ +'use client'; + +import { useEffect, useState, useRef, useCallback } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { UploadedFile } from '@/types'; +import { formatFileSize } from '@/lib/utils'; + +interface PreviewModalProps { + file: UploadedFile | null; + onClose: () => void; + onDownload: (file: UploadedFile) => void; +} + +/** Formats we can show as text in a
 block */
+const TEXT_FORMATS = new Set([
+  'txt', 'md', 'json', 'csv', 'tsv', 'xml', 'yaml', 'yml', 'toml',
+  'ini', 'env', 'properties', 'ndjson', 'jsonl', 'sql', 'rst', 'tex',
+]);
+
+/** Formats that render in an