1"use client";
2
3
11import { useEffect } from "react";
12import { useQuery } from "@tanstack/react-query";
13import { Download, ExternalLink, Loader2, ShieldAlert, X } from "lucide-react";
14import { filesApi, humanSize, kindForMime, type FileDetail } from "@/lib/files/api";
15
16interface PreviewModalProps {
17 fileId: string;
18 workspaceId?: string;
19 onClose: () => void;
20}
21
22export function PreviewModal({ fileId, workspaceId, onClose }: PreviewModalProps) {
23 const query = useQuery({
24 queryKey: ["files", fileId, "preview"],
25 queryFn: () => filesApi.get(fileId, { workspaceId }),
26 });
27
28 useEffect(() => {
29 const onKey = (e: KeyboardEvent) => {
30 if (e.key === "Escape") onClose();
31 };
32 window.addEventListener("keydown", onKey);
33 return () => window.removeEventListener("keydown", onKey);
34 }, [onClose]);
35
36 return (
37 <div
38 className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4"
39 role="dialog"
40 aria-modal="true"
41 aria-label={`Preview ${query.data?.name ?? "file"}`}
42 onClick={onClose}
43 >
44 <div
45 className="relative flex max-h-[90vh] w-full max-w-4xl flex-col overflow-hidden rounded-lg border border-border bg-background shadow-2xl"
46 onClick={(e) => e.stopPropagation()}
47 >
48 <div className="flex items-center justify-between gap-2 border-b border-border p-3">
49 <span className="min-w-0 flex-1 truncate text-sm font-medium">{query.data?.name ?? "Loading…"}</span>
50 <button type="button" onClick={onClose} aria-label="Close preview" className="rounded p-1 text-muted-foreground hover:bg-accent">
51 <X className="size-4" aria-hidden />
52 </button>
53 </div>
54
55 <div className="min-h-[200px] flex-1 overflow-auto bg-muted/20">
56 {query.isLoading || !query.data ? (
57 <div className="flex h-40 items-center justify-center">
58 <Loader2 className="size-5 animate-spin text-muted-foreground" aria-hidden />
59 </div>
60 ) : query.data.status === "VIRUS_DETECTED" ? (
61 <div className="flex h-40 items-center justify-center gap-2 text-sm text-red-700">
62 <ShieldAlert className="size-4" aria-hidden /> Blocked by virus scan — contact an admin
63 </div>
64 ) : query.data.status !== "READY" ? (
65 <div className="flex h-40 items-center justify-center text-sm text-muted-foreground">
66 {query.data.status === "SCANNING" ? "Scanning…" : "Processing…"}
67 </div>
68 ) : (
69 <ModalBody detail={query.data} />
70 )}
71 </div>
72
73 {query.data && query.data.status === "READY" ? (
74 <div className="flex items-center justify-end gap-2 border-t border-border p-2">
75 {kindForMime(query.data.mimeType) === "pdf" ? (
76 <a
77 href={query.data.previewUrl}
78 target="_blank"
79 rel="noopener noreferrer"
80 className="inline-flex items-center gap-1 rounded-md border border-border px-2.5 py-1 text-xs font-medium hover:bg-accent"
81 >
82 <ExternalLink className="size-3" aria-hidden /> Open in new tab
83 </a>
84 ) : null}
85 <button
86 type="button"
87 onClick={() => {
88
89
90 if (query.data.downloadUrl) window.location.href = query.data.downloadUrl;
91 }}
92 className="inline-flex items-center gap-1 rounded-md border border-border px-2.5 py-1 text-xs font-medium hover:bg-accent"
93 >
94 <Download className="size-3" aria-hidden /> Download ({humanSize(query.data.size)})
95 </button>
96 </div>
97 ) : null}
98 </div>
99 </div>
100 );
101}
102
103function ModalBody({ detail }: { detail: FileDetail }) {
104 const kind = kindForMime(detail.mimeType);
105 if (kind === "image") {
106 return (
107
108 <img src={detail.previewUrl} alt={detail.name} className="mx-auto max-h-[75vh] object-contain" />
109 );
110 }
111 if (kind === "video") {
112 return (
113 <video
114 controls
115 src={detail.previewUrl}
116 poster={detail.thumbnailUrl ?? undefined}
117 className="mx-auto max-h-[75vh] bg-black"
118 aria-label={detail.name}
119 />
120 );
121 }
122 if (kind === "audio") {
123 return (
124 <div className="flex h-32 items-center justify-center">
125 <audio controls src={detail.previewUrl} className="w-3/4" aria-label={detail.name} />
126 </div>
127 );
128 }
129 if (kind === "pdf") {
130 return (
131 <iframe
132 title={detail.name}
133 src={detail.previewUrl}
134 sandbox="allow-scripts allow-popups"
135 referrerPolicy="no-referrer"
136 className="h-[75vh] w-full bg-white"
137 />
138 );
139 }
140
141 return (
142 <div className="flex h-32 flex-col items-center justify-center gap-2 text-sm text-muted-foreground">
143 <span className="text-base">{detail.name}</span>
144 <span>{[detail.mimeType, humanSize(detail.size)].join(" · ")}</span>
145 <span>No inline preview — use Download.</span>
146 </div>
147 );
148}
149