Skip to content

Commit ac22fb1

Browse files
Restyle preview and other dialogs to use shadcn (#102)
* Restyle preview dialog loading and empty states * Use Spinner and DialogDescription in Move and Share * Add DialogDescription to New folder and Rename * Align share success dialog with DialogDescription
1 parent 8a33e30 commit ac22fb1

6 files changed

Lines changed: 131 additions & 89 deletions

File tree

‎app/components/MoveDialog.tsx‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useState, useEffect } from "react";
44
import {
55
Dialog,
66
DialogContent,
7+
DialogDescription,
78
DialogFooter,
89
DialogHeader,
910
DialogTitle,
@@ -20,7 +21,6 @@ import {
2021
ensureTrailingSlash,
2122
} from "../lib/helpers";
2223
import { Folder, AlertTriangle } from "lucide-react";
23-
import LoadingSpinner from "./shared/LoadingSpinner";
2424

2525
interface MoveDialogProps {
2626
isOpen: boolean;
@@ -169,7 +169,10 @@ export default function MoveDialog({
169169
<Dialog open={isOpen} onOpenChange={(open) => { if (!open) onClose(); }}>
170170
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-lg">
171171
<DialogHeader>
172-
<DialogTitle>Move {file.name}</DialogTitle>
172+
<DialogTitle>Move</DialogTitle>
173+
<DialogDescription className="truncate">
174+
{file.name}
175+
</DialogDescription>
173176
</DialogHeader>
174177
<main className="py-2" onKeyDown={handleKeyDown}>
175178
{/* Current Location */}
@@ -201,8 +204,9 @@ export default function MoveDialog({
201204
Select a destination:
202205
</label>
203206
{isLoadingFolders ? (
204-
<div className="flex items-center justify-center py-8">
205-
<LoadingSpinner size="sm" text="Loading folders..." />
207+
<div className="flex flex-col items-center justify-center gap-2 py-8">
208+
<Spinner className="size-5" />
209+
<p className="text-sm text-muted-foreground">Loading folders...</p>
206210
</div>
207211
) : filteredFolders.length === 0 ? (
208212
<div className="text-center py-8 text-muted-foreground text-sm">
@@ -220,7 +224,7 @@ export default function MoveDialog({
220224
: "border-l-4 border-transparent"
221225
}`}
222226
>
223-
<Folder className="h-5 w-5 text-muted-foreground flex-shrink-0" />
227+
<Folder className="h-5 w-5 text-muted-foreground shrink-0" />
224228
<span className="text-sm text-foreground truncate">{folder.name}</span>
225229
</button>
226230
))}
@@ -230,7 +234,7 @@ export default function MoveDialog({
230234

231235
{filteredFolders.length > 0 && !selectedFolderUrl && (
232236
<div className="mt-4 flex items-start gap-2 text-sm text-muted-foreground bg-accent border border-border rounded-md p-3">
233-
<AlertTriangle className="h-5 w-5 text-yellow-600 flex-shrink-0 mt-0.5" />
237+
<AlertTriangle className="h-5 w-5 text-yellow-600 shrink-0 mt-0.5" />
234238
<span>Select a location to show the folder path</span>
235239
</div>
236240
)}

‎app/components/NewFolderDialog.tsx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useState, useEffect, useRef } from "react";
44
import {
55
Dialog,
66
DialogContent,
7+
DialogDescription,
78
DialogFooter,
89
DialogHeader,
910
DialogTitle,
@@ -108,6 +109,9 @@ export default function NewFolderDialog({
108109
<DialogContent className="sm:max-w-sm">
109110
<DialogHeader>
110111
<DialogTitle>New folder</DialogTitle>
112+
<DialogDescription>
113+
Enter a name for the new folder.
114+
</DialogDescription>
111115
</DialogHeader>
112116
<div className="py-2">
113117
<Input

‎app/components/PreviewModal.tsx‎

Lines changed: 93 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,24 @@ import { useState, useEffect, useRef } from "react";
44
import {
55
Dialog,
66
DialogContent,
7+
DialogDescription,
8+
DialogFooter,
79
DialogHeader,
810
DialogTitle,
911
} from "@/components/ui/dialog";
1012
import { 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";
1122
import { getFile, UrlString } from "@inrupt/solid-client";
1223
import { getAuthenticatedSession } from "../lib/helpers";
1324
import { FileItemData } from "./FileItem";
14-
import LoadingSpinner from "./shared/LoadingSpinner";
1525
import { getHttpStatus } from "../lib/helpers";
1626

1727
interface 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

Comments
 (0)