'use client'; import { useEffect, useState } from 'react'; import dynamic from 'next/dynamic'; import { Download, ExternalLink, FileWarning, ZoomIn } from 'lucide-react'; import { useQuery } from '@tanstack/react-query'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; 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; } /** * Routes a file's mime type to one of seven preview surfaces. Order * matters — `application/pdf` is matched before the generic * "application/*" bucket so PDFs stay on the rich pdfjs viewer. */ type PreviewKind = | 'image' | 'pdf' | 'text' | 'audio' | 'video' | 'office' // .docx / .xlsx / .pptx / .odt / .ods | 'unknown'; function previewKindFor(mimeType: string | undefined, fileName: string | undefined): PreviewKind { const mt = mimeType ?? ''; const name = (fileName ?? '').toLowerCase(); if (mt.startsWith('image/')) return 'image'; if (mt === 'application/pdf' || name.endsWith('.pdf')) return 'pdf'; if (mt.startsWith('audio/') || /\.(mp3|wav|m4a|ogg|flac)$/.test(name)) return 'audio'; if (mt.startsWith('video/') || /\.(mp4|mov|webm|m4v|ogg)$/.test(name)) return 'video'; if ( mt.startsWith('text/') || mt === 'application/json' || mt === 'application/xml' || /\.(txt|md|csv|tsv|json|xml|log|yaml|yml|conf|ini|html?)$/.test(name) ) { return 'text'; } if ( mt.includes('officedocument') || mt === 'application/msword' || mt === 'application/vnd.ms-excel' || mt === 'application/vnd.ms-powerpoint' || mt === 'application/vnd.oasis.opendocument.text' || mt === 'application/vnd.oasis.opendocument.spreadsheet' || mt === 'application/vnd.oasis.opendocument.presentation' || /\.(docx?|xlsx?|pptx?|odt|ods|odp)$/.test(name) ) { return 'office'; } return 'unknown'; } 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 kind = previewKindFor(mimeType, fileName); 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 && kind === 'image' && ( )} {!loading && !error && previewUrl && kind === 'pdf' && ( )} {!loading && !error && previewUrl && kind === 'text' && } {!loading && !error && previewUrl && kind === 'audio' && (
{/* HTML5
)} {!loading && !error && previewUrl && kind === 'video' && (
)} {!loading && !error && previewUrl && kind === 'office' && ( // Office documents render via Microsoft's hosted Office viewer // — public URL only; presigned download URLs include a token // in the query string so they work here even though the file // isn't world-public. The viewer streams the document and // renders a high-fidelity preview without us shipping a // headless LibreOffice. Falls back to "download to view" if // the embed loads but renders nothing (e.g. CORS rejected) — // detection is hard so we just keep the download CTA below.