'use client'; import { useState } from 'react'; import dynamic from 'next/dynamic'; import { ExternalLink, ZoomIn } from 'lucide-react'; import { useQuery } from '@tanstack/react-query'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { apiFetch } from '@/lib/api/client'; // yet-another-react-lightbox is ~50kb, lazy-load it. const Lightbox = dynamic(() => import('yet-another-react-lightbox'), { ssr: false }); import 'yet-another-react-lightbox/styles.css'; // pdfjs-dist is ~150kb gzip — lazy-load so routes that never preview // PDFs don't ship it. ssr:false because the worker setup needs window. const PdfViewer = dynamic(() => import('./pdf-viewer').then((m) => ({ default: m.PdfViewer })), { ssr: false, loading: () => (
Loading PDF viewer…
), }); interface FilePreviewDialogProps { open: boolean; onOpenChange: (open: boolean) => void; fileId?: string; fileName?: string; mimeType?: string; } export function FilePreviewDialog({ open, onOpenChange, fileId, fileName, mimeType, }: FilePreviewDialogProps) { const [lightboxOpen, setLightboxOpen] = useState(false); // useQuery replaces the prior useEffect(fetch+setState) pattern. The // request is gated on the dialog being open and a fileId being set. const previewQuery = useQuery<{ data: { url: string } }>({ queryKey: ['file-preview', fileId], queryFn: () => apiFetch(`/api/v1/files/${fileId}/preview`), enabled: open && !!fileId, }); const previewUrl = previewQuery.data?.data.url ?? null; const loading = previewQuery.isLoading; const error = previewQuery.error ? 'Failed to load preview' : null; const isImage = mimeType?.startsWith('image/'); const isPdf = mimeType === 'application/pdf'; return ( {fileName ?? 'Preview'} {previewUrl && ( )} {/* A6: screen-reader description; visually hidden because the * title + preview surface tells sighted users what the dialog * contains. Skips the Radix "missing aria-describedby" warning. */} Inline preview of {fileName ?? 'the selected file'}.
{loading && (
Loading preview...
)} {error && (
{error}
)} {!loading && !error && previewUrl && isImage && ( )} {!loading && !error && previewUrl && isPdf && ( )}
{/* Lightbox renders OUTSIDE the parent Dialog so the dialog's own * bounds don't clip the fullscreen overlay. yet-another-react- * lightbox handles zoom/pan/keyboard nav out of the box. */} {previewUrl && isImage && ( setLightboxOpen(false)} slides={[{ src: previewUrl, alt: fileName ?? 'Preview' }]} controller={{ closeOnBackdropClick: true }} carousel={{ finite: true }} /> )}
); }