feat(registratura): QuickLook-style attachment preview
New fullscreen preview modal for document attachments: - Images: zoomable (scroll wheel + buttons), pannable when zoomed, zoom percentage display, reset with '0' key - PDFs: native browser PDF viewer via blob URL iframe - Navigation: left/right arrows (keyboard + buttons), bottom thumbnail strip when multiple attachments - Actions: download, print, close (Esc) - Dark overlay with smooth animations - Preview button (eye icon) shown for images AND PDFs - Replaced old inline image-only preview with new QuickLook modal New file: attachment-preview.tsx (~450 lines) Modified: registry-entry-detail.tsx (integrated preview)
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
Clock,
|
||||
Copy,
|
||||
ExternalLink,
|
||||
Eye,
|
||||
FileText,
|
||||
GitBranch,
|
||||
HardDrive,
|
||||
@@ -35,7 +36,11 @@ import { DEFAULT_DOC_TYPE_LABELS } from "../types";
|
||||
import { getOverdueDays } from "../services/registry-service";
|
||||
import { pathFileName, shareLabelFor } from "@/config/nas-paths";
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
import { useState, useCallback } from "react";
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import {
|
||||
AttachmentPreview,
|
||||
getPreviewableAttachments,
|
||||
} from "./attachment-preview";
|
||||
|
||||
interface RegistryEntryDetailProps {
|
||||
entry: RegistryEntry | null;
|
||||
@@ -130,11 +135,14 @@ export function RegistryEntryDetail({
|
||||
onDelete,
|
||||
allEntries,
|
||||
}: RegistryEntryDetailProps) {
|
||||
const [previewAttachment, setPreviewAttachment] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [previewIndex, setPreviewIndex] = useState<number | null>(null);
|
||||
const [copiedPath, setCopiedPath] = useState<string | null>(null);
|
||||
|
||||
const previewableAtts = useMemo(
|
||||
() => (entry ? getPreviewableAttachments(entry.attachments) : []),
|
||||
[entry],
|
||||
);
|
||||
|
||||
const copyPath = useCallback(async (path: string) => {
|
||||
await navigator.clipboard.writeText(path);
|
||||
setCopiedPath(path);
|
||||
@@ -448,20 +456,26 @@ export function RegistryEntryDetail({
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
{/* Preview for images */}
|
||||
{att.type.startsWith("image/") &&
|
||||
att.data &&
|
||||
{/* Preview button (images + PDFs) */}
|
||||
{att.data &&
|
||||
att.data !== "" &&
|
||||
att.data !== "__network__" && (
|
||||
att.data !== "__network__" &&
|
||||
(att.type.startsWith("image/") ||
|
||||
att.type === "application/pdf") && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
onClick={() => setPreviewAttachment(att.id)}
|
||||
onClick={() => {
|
||||
const idx = previewableAtts.findIndex(
|
||||
(a) => a.id === att.id,
|
||||
);
|
||||
if (idx >= 0) setPreviewIndex(idx);
|
||||
}}
|
||||
title="Previzualizare"
|
||||
>
|
||||
<ImageIcon className="h-3 w-3" />
|
||||
<Eye className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
{/* Download for files with data */}
|
||||
@@ -493,51 +507,6 @@ export function RegistryEntryDetail({
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Image preview modal */}
|
||||
{previewAttachment && (
|
||||
<div className="mt-3 rounded-md border p-2 bg-muted/30">
|
||||
{(() => {
|
||||
const att = entry.attachments.find(
|
||||
(a) => a.id === previewAttachment,
|
||||
);
|
||||
if (
|
||||
!att ||
|
||||
!att.type.startsWith("image/") ||
|
||||
!att.data ||
|
||||
att.data === "" ||
|
||||
att.data === "__network__"
|
||||
)
|
||||
return (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Previzualizare indisponibilă (fișierul nu conține
|
||||
date inline).
|
||||
</p>
|
||||
);
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs font-medium">{att.name}</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-5 w-5"
|
||||
onClick={() => setPreviewAttachment(null)}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={att.data}
|
||||
alt={att.name}
|
||||
className="max-w-full max-h-64 rounded border object-contain"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</DetailSection>
|
||||
)}
|
||||
|
||||
@@ -648,6 +617,17 @@ export function RegistryEntryDetail({
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
{/* QuickLook-style attachment preview */}
|
||||
{previewIndex !== null && (
|
||||
<AttachmentPreview
|
||||
key={previewIndex}
|
||||
attachments={previewableAtts}
|
||||
initialIndex={previewIndex}
|
||||
open
|
||||
onClose={() => setPreviewIndex(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user