@@ -4,14 +4,24 @@ import { useState, useEffect, useRef } from "react";
44import {
55 Dialog ,
66 DialogContent ,
7+ DialogDescription ,
8+ DialogFooter ,
79 DialogHeader ,
810 DialogTitle ,
911} from "@/components/ui/dialog" ;
1012import { Button } from "@/components/ui/button" ;
13+ import { Spinner } from "@/components/ui/spinner" ;
14+ import {
15+ Empty ,
16+ EmptyDescription ,
17+ EmptyHeader ,
18+ EmptyMedia ,
19+ EmptyTitle ,
20+ } from "@/components/ui/empty" ;
21+ import { AlertCircle , FileWarning } from "lucide-react" ;
1122import { getFile , UrlString } from "@inrupt/solid-client" ;
1223import { getAuthenticatedSession } from "../lib/helpers" ;
1324import { FileItemData } from "./FileItem" ;
14- import LoadingSpinner from "./shared/LoadingSpinner" ;
1525import { getHttpStatus } from "../lib/helpers" ;
1626
1727interface PreviewModalProps {
@@ -64,38 +74,38 @@ export default function PreviewModal({
6474
6575 try {
6676 const { fetch : fetchFn } = getAuthenticatedSession ( ) ;
67-
77+
6878
6979 let fileBlob : Blob ;
7080 let actualMimeType : string = "" ;
71-
81+
7282 try {
7383 fileBlob = await getFile ( file . url as UrlString , { fetch : fetchFn } ) ;
7484 // Get the content-type from the blob's type property (set from HTTP response header)
7585 actualMimeType = fileBlob . type ? fileBlob . type . split ( ";" ) [ 0 ] . trim ( ) : "" ;
7686 } catch ( getFileError : unknown ) {
7787 const statusCode = getHttpStatus ( getFileError ) ;
7888 const errorMessage = getFileError instanceof Error ? getFileError . message : String ( getFileError ) ;
79-
80- if ( statusCode === 501 ||
81- errorMessage . includes ( "501" ) ||
82- errorMessage . includes ( "Not Implemented" ) ||
83- errorMessage . includes ( "No conversion path" ) ) {
89+
90+ if ( statusCode === 501 ||
91+ errorMessage . includes ( "501" ) ||
92+ errorMessage . includes ( "Not Implemented" ) ||
93+ errorMessage . includes ( "No conversion path" ) ) {
8494
8595 const response = await fetchFn ( file . url ) ;
8696
8797 if ( response . status === 501 ) {
88-
98+
8999 const contentType = response . headers . get ( "content-type" ) || "" ;
90-
100+
91101 if ( contentType . includes ( "text/turtle" ) || contentType . includes ( "application/json" ) ) {
92102 // Can't preview - server returned error instead of file
93103 setFileType ( "other" ) ;
94104 setPreviewUnavailableReason ( "This binary file cannot be converted to a previewable format by the server. Please download the file to view it." ) ;
95105 setIsLoading ( false ) ;
96106 return ;
97107 }
98-
108+
99109 try {
100110 fileBlob = await response . blob ( ) ;
101111 actualMimeType = contentType . split ( ";" ) [ 0 ] . trim ( ) ;
@@ -115,26 +125,26 @@ export default function PreviewModal({
115125 throw getFileError ;
116126 }
117127 }
118-
128+
119129 if ( blobUrlRef . current ) {
120130 URL . revokeObjectURL ( blobUrlRef . current ) ;
121131 }
122-
132+
123133 const blobUrl = URL . createObjectURL ( fileBlob ) ;
124134 blobUrlRef . current = blobUrl ;
125-
135+
126136 // PDFs: open in new tab
127137 if ( actualMimeType === "application/pdf" ) {
128138 window . open ( blobUrl , "_blank" ) ;
129139 onClose ( ) ;
130140 setIsLoading ( false ) ;
131141 return ;
132142 }
133-
143+
134144 // Word documents: browsers can't natively view them, trigger download
135- if ( actualMimeType . startsWith ( "application/msword" ) ||
136- actualMimeType . includes ( "wordprocessingml" ) ||
137- actualMimeType . includes ( "ms-word" ) ) {
145+ if ( actualMimeType . startsWith ( "application/msword" ) ||
146+ actualMimeType . includes ( "wordprocessingml" ) ||
147+ actualMimeType . includes ( "ms-word" ) ) {
138148 const link = document . createElement ( "a" ) ;
139149 link . href = blobUrl ;
140150 link . download = file . name ;
@@ -146,19 +156,19 @@ export default function PreviewModal({
146156 setIsLoading ( false ) ;
147157 return ;
148158 }
149-
159+
150160 // Images: display in modal
151161 if ( actualMimeType . startsWith ( "image/" ) ) {
152162 setPreviewUrl ( blobUrl ) ;
153163 setFileType ( "image" ) ;
154164 setIsLoading ( false ) ;
155165 return ;
156166 }
157-
167+
158168 // Check if this is a binary file that shouldn't be read as text
159169 let isBinaryFile = false ;
160170 let reason = "" ;
161-
171+
162172 if ( file . name . endsWith ( ".DS_Store" ) || file . name . endsWith ( ".ds_store" ) ) {
163173 isBinaryFile = true ;
164174 reason = "This is a macOS system file (binary format) that cannot be displayed as text." ;
@@ -168,30 +178,30 @@ export default function PreviewModal({
168178 } else if ( actualMimeType . includes ( "binary" ) ) {
169179 isBinaryFile = true ;
170180 reason = "This is a binary file that cannot be displayed as text." ;
171- } else if ( actualMimeType &&
172- ! actualMimeType . startsWith ( "text/" ) &&
173- ! actualMimeType . includes ( "json" ) &&
174- ! actualMimeType . includes ( "xml" ) &&
175- ! actualMimeType . includes ( "javascript" ) &&
176- ! actualMimeType . includes ( "yaml" ) &&
177- ! actualMimeType . includes ( "csv" ) &&
178- actualMimeType !== "application/pdf" &&
179- ! actualMimeType . startsWith ( "image/" ) &&
180- ! actualMimeType . startsWith ( "application/msword" ) &&
181- ! actualMimeType . includes ( "wordprocessingml" ) &&
182- ! actualMimeType . includes ( "ms-word" ) ) {
181+ } else if ( actualMimeType &&
182+ ! actualMimeType . startsWith ( "text/" ) &&
183+ ! actualMimeType . includes ( "json" ) &&
184+ ! actualMimeType . includes ( "xml" ) &&
185+ ! actualMimeType . includes ( "javascript" ) &&
186+ ! actualMimeType . includes ( "yaml" ) &&
187+ ! actualMimeType . includes ( "csv" ) &&
188+ actualMimeType !== "application/pdf" &&
189+ ! actualMimeType . startsWith ( "image/" ) &&
190+ ! actualMimeType . startsWith ( "application/msword" ) &&
191+ ! actualMimeType . includes ( "wordprocessingml" ) &&
192+ ! actualMimeType . includes ( "ms-word" ) ) {
183193 isBinaryFile = true ;
184194 reason = `This file type (${ actualMimeType } ) is not supported for preview. Please download the file to view it.` ;
185195 }
186-
196+
187197 if ( isBinaryFile ) {
188-
198+
189199 setFileType ( "other" ) ;
190200 setPreviewUnavailableReason ( reason ) ;
191201 setIsLoading ( false ) ;
192202 return ;
193203 }
194-
204+
195205 try {
196206 const text = await fileBlob . text ( ) ;
197207 if ( text . length > 0 && text . length < 10 * 1024 * 1024 ) { // Less than 10MB
@@ -203,7 +213,7 @@ export default function PreviewModal({
203213 } catch ( err ) {
204214 console . error ( "Failed to load preview:" , err ) ;
205215 }
206-
216+
207217 setFileType ( "other" ) ;
208218 setIsLoading ( false ) ;
209219 } catch ( err ) {
@@ -221,39 +231,44 @@ export default function PreviewModal({
221231 const renderPreview = ( ) => {
222232 if ( isLoading ) {
223233 return (
224- < div className = "flex h-96 items-center justify-center" >
225- < LoadingSpinner size = "md" text = "Loading preview..." />
234+ < div className = "flex min-h-64 flex-col items-center justify-center gap-3 py-12" >
235+ < Spinner className = "size-6" />
236+ < p className = "text-sm text-muted-foreground" > Loading preview...</ p >
226237 </ div >
227238 ) ;
228239 }
229240
230241 if ( error ) {
231242 return (
232- < div className = "flex h-96 flex-col items-center justify-center text-center" >
233- < p className = "text-red-600 mb-4" > { error } </ p >
234- < Button variant = "default" onClick = { onClose } >
235- Close
236- </ Button >
237- </ div >
243+ < Empty className = "min-h-64 border-0 py-12" >
244+ < EmptyHeader >
245+ < EmptyMedia variant = "icon" >
246+ < AlertCircle className = "text-destructive" />
247+ </ EmptyMedia >
248+ < EmptyTitle > Preview failed</ EmptyTitle >
249+ < EmptyDescription > { error } </ EmptyDescription >
250+ </ EmptyHeader >
251+ </ Empty >
238252 ) ;
239253 }
240254
241255 if ( fileType === "image" ) {
242256 if ( ! previewUrl ) {
243257 return (
244- < div className = "flex h-96 items-center justify-center" >
245- < LoadingSpinner size = "md" text = "Loading image..." />
258+ < div className = "flex min-h-64 flex-col items-center justify-center gap-3 py-12" >
259+ < Spinner className = "size-6" />
260+ < p className = "text-sm text-muted-foreground" > Loading image...</ p >
246261 </ div >
247262 ) ;
248263 }
249264 return (
250- < div className = "flex min-h-[80vh] items-center justify-center bg-muted p-4" >
265+ < div className = "flex min-h-64 items-center justify-center rounded-2xl bg-muted p-4" >
251266 { /* Solid preview URLs are authenticated/cross-origin; next/image is not suitable here */ }
252267 { /* eslint-disable-next-line @next/next/no-img-element */ }
253268 < img
254269 src = { previewUrl }
255270 alt = { file . name }
256- className = "max-h-[80vh ] max-w-full object-contain"
271+ className = "max-h-[60vh ] max-w-full object-contain"
257272 onError = { ( ) => setError ( "Failed to load image" ) }
258273 />
259274 </ div >
@@ -262,42 +277,49 @@ export default function PreviewModal({
262277
263278 if ( fileType === "text" ) {
264279 return (
265- < div className = "min-h-[80vh] overflow-auto" >
266- < pre className = "whitespace-pre-wrap break-words p-4 font-mono text-sm text-foreground bg-muted rounded " >
280+ < div className = "min-h-64 overflow-auto rounded-2xl bg-muted " >
281+ < pre className = "wrap-break-word whitespace-pre-wrap p-4 font-mono text-sm text-foreground" >
267282 { previewContent || "" }
268283 </ pre >
269284 </ div >
270285 ) ;
271286 }
272287
273- // For other file types
274288 return (
275- < div className = "flex h-96 flex-col items-center justify-center text-center px-4" >
276- < p className = "text-muted-foreground mb-2 font-medium" >
277- Preview is not available for this file type.
278- </ p >
279- { previewUnavailableReason && (
280- < p className = "text-sm text-muted-foreground mb-4 max-w-md" >
281- { previewUnavailableReason }
282- </ p >
283- ) }
284- < p className = "text-sm text-muted-foreground mb-6" >
285- Please download the file to view it.
286- </ p >
287- < Button variant = "default" onClick = { onClose } >
288- Close
289- </ Button >
290- </ div >
289+ < Empty className = "min-h-64 border-0 py-12" >
290+ < EmptyHeader >
291+ < EmptyMedia variant = "icon" >
292+ < FileWarning />
293+ </ EmptyMedia >
294+ < EmptyTitle > Preview not available</ EmptyTitle >
295+ < EmptyDescription >
296+ { previewUnavailableReason ||
297+ "This file type cannot be previewed in the browser. Download the file to view it." }
298+ </ EmptyDescription >
299+ </ EmptyHeader >
300+ </ Empty >
291301 ) ;
292302 } ;
293303
294304 return (
295305 < Dialog open = { isOpen } onOpenChange = { ( open ) => { if ( ! open ) onClose ( ) ; } } >
296- < DialogContent className = "max-h-[90vh] overflow-y-auto sm:max-w-6xl " >
306+ < DialogContent className = "flex max-h-[90vh] flex-col gap-4 overflow-hidden sm:max-w-4xl " >
297307 < DialogHeader >
298- < DialogTitle > Preview: { file . name } </ DialogTitle >
308+ < DialogTitle > Preview</ DialogTitle >
309+ < DialogDescription className = "truncate" >
310+ { file . name }
311+ </ DialogDescription >
299312 </ DialogHeader >
300- { renderPreview ( ) }
313+
314+ < div className = "min-h-0 flex-1 overflow-auto" >
315+ { renderPreview ( ) }
316+ </ div >
317+
318+ < DialogFooter >
319+ < Button variant = "outline" onClick = { onClose } >
320+ Close
321+ </ Button >
322+ </ DialogFooter >
301323 </ DialogContent >
302324 </ Dialog >
303325 ) ;
0 commit comments