Skip to content

Commit f739858

Browse files
feat: implemented file upload, and folder creation
1 parent be90b93 commit f739858

15 files changed

Lines changed: 775 additions & 82 deletions

app/components/FileManager.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import Sidebar from "./Sidebar";
88
import Breadcrumb from "./Breadcrumb";
99
import FileList from "./FileList";
1010
import PermissionsDialog, { Permission } from "./PermissionsDialog";
11+
import NewFolderDialog from "./NewFolderDialog";
12+
import FileUploadHandler from "./FileUploadHandler";
1113
import { FileItemData } from "./FileItem";
1214
import { useSolidStorages, useBrowseStorage } from "../lib/hooks";
1315
import { filterProfileItems, buildBreadcrumbItems } from "../lib/helpers";
@@ -176,12 +178,23 @@ export default function FileManager() {
176178
: currentPath
177179
: null;
178180

179-
const { files: browsedFiles, isLoading: isLoadingFiles, error: browseError } = useBrowseStorage(containerUrlToBrowse);
181+
const [refreshKey, setRefreshKey] = useState(0);
182+
const { files: browsedFiles, isLoading: isLoadingFiles, error: browseError } = useBrowseStorage(containerUrlToBrowse, refreshKey);
180183
const [permissionsDialogOpen, setPermissionsDialogOpen] = useState(false);
181184
const [selectedFileForPermissions, setSelectedFileForPermissions] =
182185
useState<FileItemData | null>(null);
183186
const [permissions, setPermissions] = useState<Permission[]>([]);
184187
const [sidebarOpen, setSidebarOpen] = useState(false);
188+
const [showNewFolderDialog, setShowNewFolderDialog] = useState(false);
189+
const [fileUploadTrigger, setFileUploadTrigger] = useState(0);
190+
191+
const handleFolderCreated = () => {
192+
setRefreshKey((prev) => prev + 1);
193+
};
194+
195+
const handleFileUploaded = () => {
196+
setRefreshKey((prev) => prev + 1);
197+
};
185198

186199
const storageFiles: FileItemData[] = filterProfileItems(storages).map((storage) => ({
187200
id: storage.id,
@@ -347,6 +360,9 @@ export default function FileManager() {
347360
isOpen={sidebarOpen}
348361
onClose={() => setSidebarOpen(false)}
349362
activeTab="my-storages"
363+
currentContainerUrl={containerUrlToBrowse}
364+
onNewFolderClick={() => setShowNewFolderDialog(true)}
365+
onFileUploadClick={() => setFileUploadTrigger((prev) => prev + 1)}
350366
/>
351367
<main className="flex flex-1 flex-col overflow-hidden">
352368
<Breadcrumb items={breadcrumbItems} onNavigate={handleBreadcrumbNavigate} />
@@ -379,6 +395,17 @@ export default function FileManager() {
379395
onUpdatePermission={handleUpdatePermission}
380396
/>
381397
)}
398+
<NewFolderDialog
399+
isOpen={showNewFolderDialog}
400+
onClose={() => setShowNewFolderDialog(false)}
401+
currentContainerUrl={containerUrlToBrowse}
402+
onFolderCreated={handleFolderCreated}
403+
/>
404+
<FileUploadHandler
405+
currentContainerUrl={containerUrlToBrowse}
406+
onUploadComplete={handleFileUploaded}
407+
triggerUpload={fileUploadTrigger}
408+
/>
382409
</div>
383410
</AuthWrapper>
384411
);
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"use client";
2+
3+
import { useRef, useEffect } from "react";
4+
import { getDefaultSession } from "@inrupt/solid-client-authn-browser";
5+
import { overwriteFile, UrlString } from "@inrupt/solid-client";
6+
import toast from "react-hot-toast";
7+
8+
interface FileUploadHandlerProps {
9+
currentContainerUrl: string | null;
10+
onUploadComplete?: () => void;
11+
triggerUpload?: number;
12+
}
13+
14+
export default function FileUploadHandler({
15+
currentContainerUrl,
16+
onUploadComplete,
17+
triggerUpload,
18+
}: FileUploadHandlerProps) {
19+
const fileInputRef = useRef<HTMLInputElement>(null);
20+
21+
useEffect(() => {
22+
if (triggerUpload && triggerUpload > 0 && fileInputRef.current) {
23+
fileInputRef.current.click();
24+
}
25+
}, [triggerUpload]);
26+
27+
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
28+
const files = e.target.files;
29+
if (!files || files.length === 0) return;
30+
31+
if (!currentContainerUrl) {
32+
toast.error("Please select a storage first");
33+
e.target.value = "";
34+
return;
35+
}
36+
37+
const session = getDefaultSession();
38+
if (!session.info.isLoggedIn) {
39+
toast.error("Not authenticated");
40+
e.target.value = "";
41+
return;
42+
}
43+
44+
const fetchFn = session.fetch || fetch;
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);
73+
}
74+
75+
try {
76+
await Promise.all(uploadPromises);
77+
78+
if (uploadedFiles.length > 0) {
79+
const message =
80+
uploadedFiles.length === 1
81+
? `File uploaded successfully`
82+
: `${uploadedFiles.length} files uploaded successfully`;
83+
toast.success(message);
84+
}
85+
86+
if (failedFiles.length > 0) {
87+
const message =
88+
failedFiles.length === 1
89+
? `Failed to upload "${failedFiles[0]}"`
90+
: `Failed to upload ${failedFiles.length} files`;
91+
toast.error(message);
92+
}
93+
94+
if (uploadedFiles.length > 0 && onUploadComplete) {
95+
await new Promise((resolve) => setTimeout(resolve, 200));
96+
onUploadComplete();
97+
}
98+
} catch (error) {
99+
console.error("Upload error:", error);
100+
toast.error("Failed to upload files");
101+
} finally {
102+
e.target.value = "";
103+
}
104+
};
105+
106+
return (
107+
<input
108+
ref={fileInputRef}
109+
type="file"
110+
multiple
111+
className="hidden"
112+
onChange={handleFileChange}
113+
/>
114+
);
115+
}
116+

app/components/Header.tsx

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import ProfileIcon from "./ProfileIcon";
88
import {
99
Bars3Icon,
1010
MagnifyingGlassIcon,
11-
PlusIcon,
1211
} from "@heroicons/react/24/outline";
1312

1413
interface HeaderProps {
@@ -51,16 +50,6 @@ export default function Header({ onMenuClick }: HeaderProps) {
5150

5251
{/* Action Buttons */}
5352
<div className="ml-auto flex items-center gap-1 sm:gap-2">
54-
<Button
55-
variant="secondary"
56-
size="sm"
57-
className="flex h-9 items-center gap-1 sm:gap-2 sm:px-3"
58-
aria-label="New"
59-
>
60-
<PlusIcon className="h-4 w-4" />
61-
<span className="hidden sm:inline">New</span>
62-
</Button>
63-
6453
<ProfileIcon />
6554
</div>
6655
</div>

app/components/NewFolderDialog.tsx

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"use client";
2+
3+
import { useState, useEffect, useRef } from "react";
4+
import Modal from "./shared/Modal";
5+
import Button from "./shared/Button";
6+
import { getDefaultSession } from "@inrupt/solid-client-authn-browser";
7+
import { createContainerAt, getSolidDataset, UrlString } from "@inrupt/solid-client";
8+
import toast from "react-hot-toast";
9+
10+
interface NewFolderDialogProps {
11+
isOpen: boolean;
12+
onClose: () => void;
13+
currentContainerUrl: string | null;
14+
onFolderCreated?: () => void;
15+
}
16+
17+
export default function NewFolderDialog({
18+
isOpen,
19+
onClose,
20+
currentContainerUrl,
21+
onFolderCreated,
22+
}: NewFolderDialogProps) {
23+
const [folderName, setFolderName] = useState("Untitled folder");
24+
const [isCreating, setIsCreating] = useState(false);
25+
const inputRef = useRef<HTMLInputElement>(null);
26+
27+
useEffect(() => {
28+
if (isOpen) {
29+
setFolderName("Untitled folder");
30+
setIsCreating(false);
31+
// Focus and select the input text when modal opens
32+
setTimeout(() => {
33+
if (inputRef.current) {
34+
inputRef.current.focus();
35+
inputRef.current.select();
36+
}
37+
}, 100);
38+
}
39+
}, [isOpen]);
40+
41+
const handleCreate = async () => {
42+
if (!folderName.trim()) {
43+
toast.error("Please enter a folder name");
44+
return;
45+
}
46+
47+
if (!currentContainerUrl) {
48+
toast.error("Please select a storage first");
49+
return;
50+
}
51+
52+
setIsCreating(true);
53+
54+
try {
55+
const session = getDefaultSession();
56+
if (!session.info.isLoggedIn) {
57+
throw new Error("Not authenticated");
58+
}
59+
60+
const fetchFn = session.fetch || fetch;
61+
62+
// Ensure the current container exists
63+
await getSolidDataset(currentContainerUrl, { fetch: fetchFn });
64+
65+
// Create the new folder URL
66+
const sanitizedName = folderName.trim().replace(/[<>:"/\\|?*]/g, "");
67+
const newFolderUrl = currentContainerUrl.endsWith("/")
68+
? `${currentContainerUrl}${sanitizedName}/`
69+
: `${currentContainerUrl}/${sanitizedName}/`;
70+
71+
// Create the container
72+
await createContainerAt(newFolderUrl as UrlString, { fetch: fetchFn });
73+
74+
// Small delay to ensure server has processed the creation
75+
await new Promise(resolve => setTimeout(resolve, 200));
76+
77+
toast.success(`Folder "${sanitizedName}" created successfully`);
78+
79+
// Notify parent to refresh before closing
80+
if (onFolderCreated) {
81+
onFolderCreated();
82+
}
83+
84+
onClose();
85+
} catch (error) {
86+
console.error("Failed to create folder:", error);
87+
toast.error(
88+
error instanceof Error
89+
? `Failed to create folder: ${error.message}`
90+
: "Failed to create folder"
91+
);
92+
} finally {
93+
setIsCreating(false);
94+
}
95+
};
96+
97+
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
98+
if (e.key === "Enter" && !isCreating) {
99+
handleCreate();
100+
} else if (e.key === "Escape") {
101+
onClose();
102+
}
103+
};
104+
105+
return (
106+
<Modal
107+
isOpen={isOpen}
108+
onClose={onClose}
109+
title="New folder"
110+
maxWidth="sm"
111+
footer={
112+
<div className="flex justify-end gap-2">
113+
<Button
114+
variant="ghost"
115+
onClick={onClose}
116+
disabled={isCreating}
117+
>
118+
Cancel
119+
</Button>
120+
<Button
121+
variant="primary"
122+
onClick={handleCreate}
123+
isLoading={isCreating}
124+
disabled={isCreating || !folderName.trim()}
125+
>
126+
Create
127+
</Button>
128+
</div>
129+
}
130+
>
131+
<div className="py-2">
132+
<input
133+
ref={inputRef}
134+
type="text"
135+
value={folderName}
136+
onChange={(e) => setFolderName(e.target.value)}
137+
onKeyDown={handleKeyDown}
138+
placeholder="Untitled folder"
139+
className="w-full h-10 rounded-md border border-gray-300 bg-white px-3 text-sm text-black placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-[#7B42F6] focus:border-[#7B42F6] transition-colors disabled:bg-gray-50 disabled:text-gray-500"
140+
disabled={isCreating}
141+
/>
142+
</div>
143+
</Modal>
144+
);
145+
}
146+

0 commit comments

Comments
 (0)