Skip to content

Commit 5902e46

Browse files
Merge pull request #20 from solid/feat/my-storages
Feat/my storages
2 parents 7a5ac65 + 1cf1a16 commit 5902e46

12 files changed

Lines changed: 968 additions & 250 deletions

app/components/FileManager.tsx

Lines changed: 234 additions & 135 deletions
Large diffs are not rendered by default.
Lines changed: 97 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,42 @@
11
"use client";
22

33
import { useRef, useEffect } from "react";
4-
import { overwriteFile, UrlString } from "@inrupt/solid-client";
54
import toast from "react-hot-toast";
6-
import { getAuthenticatedSession } from "../lib/helpers";
5+
import {
6+
getAuthenticatedSession,
7+
uploadFilesToContainer,
8+
uploadFolderFilesToContainer,
9+
FolderUploadFile,
10+
} from "../lib/helpers";
711

812
interface FileUploadHandlerProps {
913
currentContainerUrl: string | null;
1014
onUploadComplete?: () => void;
1115
triggerUpload?: number;
16+
triggerFolderUpload?: number;
1217
}
1318

1419
export default function FileUploadHandler({
1520
currentContainerUrl,
1621
onUploadComplete,
1722
triggerUpload,
23+
triggerFolderUpload,
1824
}: FileUploadHandlerProps) {
1925
const fileInputRef = useRef<HTMLInputElement>(null);
26+
const folderInputRef = useRef<HTMLInputElement>(null);
2027

2128
useEffect(() => {
2229
if (triggerUpload && triggerUpload > 0 && fileInputRef.current) {
2330
fileInputRef.current.click();
2431
}
2532
}, [triggerUpload]);
2633

34+
useEffect(() => {
35+
if (triggerFolderUpload && triggerFolderUpload > 0 && folderInputRef.current) {
36+
folderInputRef.current.click();
37+
}
38+
}, [triggerFolderUpload]);
39+
2740
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
2841
const files = e.target.files;
2942
if (!files || files.length === 0) return;
@@ -42,38 +55,73 @@ export default function FileUploadHandler({
4255
e.target.value = "";
4356
return;
4457
}
45-
const uploadPromises: Promise<void>[] = [];
46-
const uploadedFiles: string[] = [];
47-
const failedFiles: string[] = [];
48-
49-
for (let i = 0; i < files.length; i++) {
50-
const file = files[i];
51-
const sanitizedName = file.name.replace(/[<>:"/\\|?*]/g, "");
52-
const fileUrl = currentContainerUrl.endsWith("/")
53-
? `${currentContainerUrl}${sanitizedName}`
54-
: `${currentContainerUrl}/${sanitizedName}`;
55-
56-
const uploadPromise = overwriteFile(
57-
fileUrl as UrlString,
58-
file,
59-
{
60-
contentType: file.type || "application/octet-stream",
61-
fetch: fetchFn,
62-
}
63-
)
64-
.then(() => {
65-
uploadedFiles.push(sanitizedName);
66-
})
67-
.catch((error) => {
68-
console.error(`Failed to upload ${file.name}:`, error);
69-
failedFiles.push(sanitizedName);
70-
});
71-
72-
uploadPromises.push(uploadPromise);
58+
try {
59+
const { uploadedFiles, failedFiles } = await uploadFilesToContainer(
60+
Array.from(files),
61+
currentContainerUrl,
62+
fetchFn
63+
);
64+
65+
if (uploadedFiles.length > 0) {
66+
const message =
67+
uploadedFiles.length === 1
68+
? `File uploaded successfully`
69+
: `${uploadedFiles.length} files uploaded successfully`;
70+
toast.success(message);
71+
}
72+
73+
if (failedFiles.length > 0) {
74+
const message =
75+
failedFiles.length === 1
76+
? `Failed to upload "${failedFiles[0]}"`
77+
: `Failed to upload ${failedFiles.length} files`;
78+
toast.error(message);
79+
}
80+
81+
if (uploadedFiles.length > 0 && onUploadComplete) {
82+
await new Promise((resolve) => setTimeout(resolve, 200));
83+
onUploadComplete();
84+
}
85+
} catch (error) {
86+
console.error("Upload error:", error);
87+
toast.error("Failed to upload files");
88+
} finally {
89+
e.target.value = "";
90+
}
91+
};
92+
93+
const handleFolderChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
94+
const files = e.target.files;
95+
if (!files || files.length === 0) return;
96+
97+
if (!currentContainerUrl) {
98+
toast.error("Please select a storage first");
99+
e.target.value = "";
100+
return;
73101
}
74102

103+
let fetchFn: typeof fetch;
75104
try {
76-
await Promise.all(uploadPromises);
105+
({ fetch: fetchFn } = getAuthenticatedSession());
106+
} catch (error) {
107+
toast.error("Not authenticated");
108+
e.target.value = "";
109+
return;
110+
}
111+
112+
const folderFiles: FolderUploadFile[] = Array.from(files)
113+
.map((file) => ({
114+
file,
115+
relativePath: (file as any).webkitRelativePath || file.name,
116+
}))
117+
.filter((item) => item.relativePath && item.relativePath.length > 0);
118+
119+
try {
120+
const { uploadedFiles, failedFiles } = await uploadFolderFilesToContainer(
121+
folderFiles,
122+
currentContainerUrl,
123+
fetchFn
124+
);
77125

78126
if (uploadedFiles.length > 0) {
79127
const message =
@@ -97,20 +145,30 @@ export default function FileUploadHandler({
97145
}
98146
} catch (error) {
99147
console.error("Upload error:", error);
100-
toast.error("Failed to upload files");
148+
toast.error("Failed to upload folder");
101149
} finally {
102150
e.target.value = "";
103151
}
104152
};
105153

106154
return (
107-
<input
108-
ref={fileInputRef}
109-
type="file"
110-
multiple
111-
className="hidden"
112-
onChange={handleFileChange}
113-
/>
155+
<>
156+
<input
157+
ref={fileInputRef}
158+
type="file"
159+
multiple
160+
className="hidden"
161+
onChange={handleFileChange}
162+
/>
163+
<input
164+
ref={folderInputRef}
165+
type="file"
166+
{...({ webkitdirectory: "" } as any)}
167+
multiple
168+
className="hidden"
169+
onChange={handleFolderChange}
170+
/>
171+
</>
114172
);
115173
}
116174

app/components/NewMenuButton.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ interface NewMenuButtonProps {
99
currentContainerUrl: string | null;
1010
onNewFolderClick?: () => void;
1111
onFileUploadClick?: () => void;
12+
onFolderUploadClick?: () => void;
1213
}
1314

1415
export default function NewMenuButton({
1516
currentContainerUrl,
1617
onNewFolderClick,
1718
onFileUploadClick,
19+
onFolderUploadClick,
1820
}: NewMenuButtonProps) {
1921
const [showNewMenu, setShowNewMenu] = useState(false);
2022
const newMenuRef = useRef<HTMLDivElement>(null);
@@ -40,6 +42,13 @@ export default function NewMenuButton({
4042
}
4143
};
4244

45+
const handleFolderUpload = () => {
46+
setShowNewMenu(false);
47+
if (onFolderUploadClick) {
48+
onFolderUploadClick();
49+
}
50+
};
51+
4352
return (
4453
<div className="relative mb-4 px-2">
4554
<Button
@@ -77,12 +86,21 @@ export default function NewMenuButton({
7786
<button
7887
type="button"
7988
onClick={handleFileUpload}
80-
className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-100 transition-colors cursor-pointer"
89+
className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-100 transition-colors border-b border-gray-100 cursor-pointer"
8190
role="menuitem"
8291
>
8392
<ArrowUpTrayIcon className="h-5 w-5 text-gray-500" />
8493
<span>File Upload</span>
8594
</button>
95+
<button
96+
type="button"
97+
onClick={handleFolderUpload}
98+
className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-100 transition-colors cursor-pointer"
99+
role="menuitem"
100+
>
101+
<FolderPlusIcon className="h-5 w-5 text-gray-500" />
102+
<span>Folder Upload</span>
103+
</button>
86104
</div>
87105
)}
88106
</div>

0 commit comments

Comments
 (0)