Files
pn-new-crm/src/components/files/file-preview-dialog.tsx

127 lines
4.4 KiB
TypeScript
Raw Normal View History

'use client';
import { useState } from 'react';
import dynamic from 'next/dynamic';
feat(deps): Tier 2 UX polish — embla, lightbox, gestures, virtuoso, motion Installs all five Tier 2 polish deps the audit flagged. Each integrates where it adds concrete value today: - **embla-carousel-react** — shadcn-style `<Carousel>` primitive in `src/components/ui/carousel.tsx`. Available for future berth/yacht photo galleries; no current call site beyond the primitive. - **yet-another-react-lightbox** — wired into the image branch of `file-preview-dialog.tsx`. Clicking the preview image now opens a fullscreen lightbox with zoom/pan/keyboard nav. Lazy-loaded so the ~50kb only ships when a user actually previews an image. - **@use-gesture/react** — `usePinch` on the PdfViewer's content pane for native pinch-zoom on tablets/phones. Clamped to the same [50%, 300%] range as the +/- buttons; desktop wheel still scrolls. - **react-virtuoso** — installed but NOT wired. Inbox is naturally bounded by recent-notifications filter at ~10-20 items; ScrollArea handles it fine. Reserve for actual scale issues (admin audit log archive, etc.). - **motion** — installed but NOT wired. Pipeline kanban uses dnd-kit's own transforms and conflicts with motion's layout animation. @formkit/auto-animate already handles list-mutation animations elsewhere. Available for opportunistic adoption when a polish surface emerges that the existing libraries don't cover. Verified: tsc clean, vitest 1315/1315, next build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:29:22 +02:00
import { ExternalLink, ZoomIn } from 'lucide-react';
import { useQuery } from '@tanstack/react-query';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { apiFetch } from '@/lib/api/client';
feat(deps): Tier 2 UX polish — embla, lightbox, gestures, virtuoso, motion Installs all five Tier 2 polish deps the audit flagged. Each integrates where it adds concrete value today: - **embla-carousel-react** — shadcn-style `<Carousel>` primitive in `src/components/ui/carousel.tsx`. Available for future berth/yacht photo galleries; no current call site beyond the primitive. - **yet-another-react-lightbox** — wired into the image branch of `file-preview-dialog.tsx`. Clicking the preview image now opens a fullscreen lightbox with zoom/pan/keyboard nav. Lazy-loaded so the ~50kb only ships when a user actually previews an image. - **@use-gesture/react** — `usePinch` on the PdfViewer's content pane for native pinch-zoom on tablets/phones. Clamped to the same [50%, 300%] range as the +/- buttons; desktop wheel still scrolls. - **react-virtuoso** — installed but NOT wired. Inbox is naturally bounded by recent-notifications filter at ~10-20 items; ScrollArea handles it fine. Reserve for actual scale issues (admin audit log archive, etc.). - **motion** — installed but NOT wired. Pipeline kanban uses dnd-kit's own transforms and conflicts with motion's layout animation. @formkit/auto-animate already handles list-mutation animations elsewhere. Available for opportunistic adoption when a polish surface emerges that the existing libraries don't cover. Verified: tsc clean, vitest 1315/1315, next build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:29:22 +02:00
// 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: () => (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
Loading PDF viewer
</div>
),
});
interface FilePreviewDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
fileId?: string;
fileName?: string;
mimeType?: string;
}
export function FilePreviewDialog({
open,
onOpenChange,
fileId,
fileName,
mimeType,
}: FilePreviewDialogProps) {
feat(deps): Tier 2 UX polish — embla, lightbox, gestures, virtuoso, motion Installs all five Tier 2 polish deps the audit flagged. Each integrates where it adds concrete value today: - **embla-carousel-react** — shadcn-style `<Carousel>` primitive in `src/components/ui/carousel.tsx`. Available for future berth/yacht photo galleries; no current call site beyond the primitive. - **yet-another-react-lightbox** — wired into the image branch of `file-preview-dialog.tsx`. Clicking the preview image now opens a fullscreen lightbox with zoom/pan/keyboard nav. Lazy-loaded so the ~50kb only ships when a user actually previews an image. - **@use-gesture/react** — `usePinch` on the PdfViewer's content pane for native pinch-zoom on tablets/phones. Clamped to the same [50%, 300%] range as the +/- buttons; desktop wheel still scrolls. - **react-virtuoso** — installed but NOT wired. Inbox is naturally bounded by recent-notifications filter at ~10-20 items; ScrollArea handles it fine. Reserve for actual scale issues (admin audit log archive, etc.). - **motion** — installed but NOT wired. Pipeline kanban uses dnd-kit's own transforms and conflicts with motion's layout animation. @formkit/auto-animate already handles list-mutation animations elsewhere. Available for opportunistic adoption when a polish surface emerges that the existing libraries don't cover. Verified: tsc clean, vitest 1315/1315, next build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:29:22 +02:00
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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl w-full h-[80vh] flex flex-col">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 truncate">
<span className="truncate">{fileName ?? 'Preview'}</span>
{previewUrl && (
<a
href={previewUrl}
target="_blank"
rel="noopener noreferrer"
className="shrink-0 text-muted-foreground hover:text-foreground"
>
<ExternalLink className="h-4 w-4" />
</a>
)}
</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-hidden rounded-lg border bg-muted/20">
{loading && (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
Loading preview...
</div>
)}
{error && (
<div className="flex h-full items-center justify-center text-sm text-destructive">
{error}
</div>
)}
{!loading && !error && previewUrl && isImage && (
feat(deps): Tier 2 UX polish — embla, lightbox, gestures, virtuoso, motion Installs all five Tier 2 polish deps the audit flagged. Each integrates where it adds concrete value today: - **embla-carousel-react** — shadcn-style `<Carousel>` primitive in `src/components/ui/carousel.tsx`. Available for future berth/yacht photo galleries; no current call site beyond the primitive. - **yet-another-react-lightbox** — wired into the image branch of `file-preview-dialog.tsx`. Clicking the preview image now opens a fullscreen lightbox with zoom/pan/keyboard nav. Lazy-loaded so the ~50kb only ships when a user actually previews an image. - **@use-gesture/react** — `usePinch` on the PdfViewer's content pane for native pinch-zoom on tablets/phones. Clamped to the same [50%, 300%] range as the +/- buttons; desktop wheel still scrolls. - **react-virtuoso** — installed but NOT wired. Inbox is naturally bounded by recent-notifications filter at ~10-20 items; ScrollArea handles it fine. Reserve for actual scale issues (admin audit log archive, etc.). - **motion** — installed but NOT wired. Pipeline kanban uses dnd-kit's own transforms and conflicts with motion's layout animation. @formkit/auto-animate already handles list-mutation animations elsewhere. Available for opportunistic adoption when a polish surface emerges that the existing libraries don't cover. Verified: tsc clean, vitest 1315/1315, next build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:29:22 +02:00
<button
type="button"
onClick={() => setLightboxOpen(true)}
className="flex h-full w-full items-center justify-center p-4 group"
aria-label="Open in lightbox"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={previewUrl}
alt={fileName ?? 'Preview'}
feat(deps): Tier 2 UX polish — embla, lightbox, gestures, virtuoso, motion Installs all five Tier 2 polish deps the audit flagged. Each integrates where it adds concrete value today: - **embla-carousel-react** — shadcn-style `<Carousel>` primitive in `src/components/ui/carousel.tsx`. Available for future berth/yacht photo galleries; no current call site beyond the primitive. - **yet-another-react-lightbox** — wired into the image branch of `file-preview-dialog.tsx`. Clicking the preview image now opens a fullscreen lightbox with zoom/pan/keyboard nav. Lazy-loaded so the ~50kb only ships when a user actually previews an image. - **@use-gesture/react** — `usePinch` on the PdfViewer's content pane for native pinch-zoom on tablets/phones. Clamped to the same [50%, 300%] range as the +/- buttons; desktop wheel still scrolls. - **react-virtuoso** — installed but NOT wired. Inbox is naturally bounded by recent-notifications filter at ~10-20 items; ScrollArea handles it fine. Reserve for actual scale issues (admin audit log archive, etc.). - **motion** — installed but NOT wired. Pipeline kanban uses dnd-kit's own transforms and conflicts with motion's layout animation. @formkit/auto-animate already handles list-mutation animations elsewhere. Available for opportunistic adoption when a polish surface emerges that the existing libraries don't cover. Verified: tsc clean, vitest 1315/1315, next build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:29:22 +02:00
className="max-h-full max-w-full object-contain rounded transition-transform group-hover:scale-[1.02]"
/>
feat(deps): Tier 2 UX polish — embla, lightbox, gestures, virtuoso, motion Installs all five Tier 2 polish deps the audit flagged. Each integrates where it adds concrete value today: - **embla-carousel-react** — shadcn-style `<Carousel>` primitive in `src/components/ui/carousel.tsx`. Available for future berth/yacht photo galleries; no current call site beyond the primitive. - **yet-another-react-lightbox** — wired into the image branch of `file-preview-dialog.tsx`. Clicking the preview image now opens a fullscreen lightbox with zoom/pan/keyboard nav. Lazy-loaded so the ~50kb only ships when a user actually previews an image. - **@use-gesture/react** — `usePinch` on the PdfViewer's content pane for native pinch-zoom on tablets/phones. Clamped to the same [50%, 300%] range as the +/- buttons; desktop wheel still scrolls. - **react-virtuoso** — installed but NOT wired. Inbox is naturally bounded by recent-notifications filter at ~10-20 items; ScrollArea handles it fine. Reserve for actual scale issues (admin audit log archive, etc.). - **motion** — installed but NOT wired. Pipeline kanban uses dnd-kit's own transforms and conflicts with motion's layout animation. @formkit/auto-animate already handles list-mutation animations elsewhere. Available for opportunistic adoption when a polish surface emerges that the existing libraries don't cover. Verified: tsc clean, vitest 1315/1315, next build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:29:22 +02:00
<ZoomIn className="absolute right-6 bottom-6 h-6 w-6 rounded-full bg-background/80 p-1 text-foreground opacity-0 transition-opacity group-hover:opacity-100" />
</button>
)}
{!loading && !error && previewUrl && isPdf && (
<PdfViewer url={previewUrl} fileName={fileName} />
)}
</div>
</DialogContent>
feat(deps): Tier 2 UX polish — embla, lightbox, gestures, virtuoso, motion Installs all five Tier 2 polish deps the audit flagged. Each integrates where it adds concrete value today: - **embla-carousel-react** — shadcn-style `<Carousel>` primitive in `src/components/ui/carousel.tsx`. Available for future berth/yacht photo galleries; no current call site beyond the primitive. - **yet-another-react-lightbox** — wired into the image branch of `file-preview-dialog.tsx`. Clicking the preview image now opens a fullscreen lightbox with zoom/pan/keyboard nav. Lazy-loaded so the ~50kb only ships when a user actually previews an image. - **@use-gesture/react** — `usePinch` on the PdfViewer's content pane for native pinch-zoom on tablets/phones. Clamped to the same [50%, 300%] range as the +/- buttons; desktop wheel still scrolls. - **react-virtuoso** — installed but NOT wired. Inbox is naturally bounded by recent-notifications filter at ~10-20 items; ScrollArea handles it fine. Reserve for actual scale issues (admin audit log archive, etc.). - **motion** — installed but NOT wired. Pipeline kanban uses dnd-kit's own transforms and conflicts with motion's layout animation. @formkit/auto-animate already handles list-mutation animations elsewhere. Available for opportunistic adoption when a polish surface emerges that the existing libraries don't cover. Verified: tsc clean, vitest 1315/1315, next build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:29:22 +02:00
{/* 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 && (
<Lightbox
open={lightboxOpen}
close={() => setLightboxOpen(false)}
slides={[{ src: previewUrl, alt: fileName ?? 'Preview' }]}
controller={{ closeOnBackdropClick: true }}
carousel={{ finite: true }}
/>
)}
</Dialog>
);
}