Skip to content

Commit 406e86e

Browse files
committed
feat: add dataset organizer page
1 parent 05f4dcd commit 406e86e

8 files changed

Lines changed: 1933 additions & 10 deletions

File tree

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,13 @@
3333
"dayjs": "^1.11.10",
3434
"jquery": "^3.7.1",
3535
"json-stringify-safe": "^5.0.1",
36+
"jszip": "^3.10.1",
3637
"jwt-decode": "^3.1.2",
3738
"lzma": "^2.3.2",
3839
"numjs": "^0.16.1",
3940
"pako": "1.0.11",
4041
"path-browserify": "^1.0.1",
42+
"pdfjs-dist": "3.4.120",
4143
"query-string": "^8.1.0",
4244
"react": "^18.2.0",
4345
"react-dom": "^18.2.0",

src/components/Routes.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import ScrollToTop from "./ScrollToTop";
22
import CompleteProfile from "./User/CompleteProfile";
33
import CollectionDetailPage from "./User/Dashboard/CollectionDetailPage";
4+
import DatasetOrganizer from "./User/Dashboard/DatasetOrganizer";
45
import ForgotPassword from "./User/ForgotPassword";
56
import ResetPassword from "./User/ResetPassword";
67
import UserDashboard from "./User/UserDashboard";
@@ -61,11 +62,12 @@ const Routes = () => (
6162
{/* Dashboard Page */}
6263
<Route path={RoutesEnum.DASHBOARD} element={<UserDashboard />} />
6364

64-
{/* Collection detail page */}
65+
{/* pages redirect from user dashboard */}
6566
<Route
6667
path="/collections/:collectionId"
6768
element={<CollectionDetailPage />}
6869
/>
70+
<Route path="/projects/:projectId" element={<DatasetOrganizer />} />
6971
</Route>
7072
</RouterRoutes>
7173
</>
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
// src/components/DatasetOrganizer/DropZone.tsx
2+
import { processFile, processFolder, processZip } from "./utils/fileProcessors";
3+
import { CloudUpload, Add } from "@mui/icons-material";
4+
import { Box, Typography, Paper, Button } from "@mui/material";
5+
import { Colors } from "design/theme";
6+
import React, { useState, useRef } from "react";
7+
import { FileItem } from "redux/projects/types/projects.interface";
8+
9+
interface DropZoneProps {
10+
files: FileItem[];
11+
setFiles: React.Dispatch<React.SetStateAction<FileItem[]>>;
12+
selectedIds: Set<string>;
13+
setSelectedIds: React.Dispatch<React.SetStateAction<Set<string>>>;
14+
expandedIds: Set<string>;
15+
setExpandedIds: React.Dispatch<React.SetStateAction<Set<string>>>;
16+
}
17+
18+
const DropZone: React.FC<DropZoneProps> = ({
19+
files,
20+
setFiles,
21+
selectedIds,
22+
setSelectedIds,
23+
expandedIds,
24+
setExpandedIds,
25+
}) => {
26+
const [isDragging, setIsDragging] = useState(false);
27+
const fileInputRef = useRef<HTMLInputElement>(null);
28+
29+
const handleDragOver = (e: React.DragEvent) => {
30+
e.preventDefault();
31+
setIsDragging(true);
32+
};
33+
34+
const handleDragLeave = (e: React.DragEvent) => {
35+
e.preventDefault();
36+
setIsDragging(false);
37+
};
38+
39+
const handleDrop = async (e: React.DragEvent) => {
40+
e.preventDefault();
41+
setIsDragging(false);
42+
43+
const items = Array.from(e.dataTransfer.items); // detect if it is a folder
44+
const droppedFiles = Array.from(e.dataTransfer.files); // only gives file objects, can't detect folders
45+
46+
// Separate folders and files
47+
const folderEntries: any[] = [];
48+
const fileItems: File[] = [];
49+
50+
for (let i = 0; i < items.length; i++) {
51+
const entry = (items[i] as any).webkitGetAsEntry?.();
52+
if (entry && entry.isDirectory) {
53+
folderEntries.push(entry);
54+
} else if (droppedFiles[i]) {
55+
fileItems.push(droppedFiles[i]);
56+
}
57+
}
58+
59+
// Process folders
60+
for (const folderEntry of folderEntries) {
61+
const folderFiles = await processFolder(folderEntry, null);
62+
setFiles((prev) => [...prev, ...folderFiles]);
63+
}
64+
65+
// Process files
66+
for (const file of fileItems) {
67+
if (file.name.toLowerCase().endsWith(".zip")) {
68+
const zipFiles = await processZip(file);
69+
setFiles((prev) => [...prev, ...zipFiles]);
70+
} else {
71+
const fileItem = await processFile(file);
72+
setFiles((prev) => [...prev, fileItem]);
73+
}
74+
}
75+
};
76+
77+
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
78+
const selectedFiles = Array.from(e.target.files || []);
79+
80+
for (const file of selectedFiles) {
81+
if (file.name.toLowerCase().endsWith(".zip")) {
82+
const zipFiles = await processZip(file);
83+
setFiles((prev) => [...prev, ...zipFiles]);
84+
} else {
85+
const fileItem = await processFile(file);
86+
setFiles((prev) => [...prev, fileItem]);
87+
}
88+
}
89+
};
90+
91+
return (
92+
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
93+
{/* Show file count if files exist */}
94+
{files.length > 0 && (
95+
<Box>
96+
<Typography variant="h6" gutterBottom>
97+
Dataset Files
98+
</Typography>
99+
<Typography variant="body2" color="text.secondary">
100+
{files.length} file{files.length !== 1 ? "s" : ""} added
101+
</Typography>
102+
</Box>
103+
)}
104+
105+
{/* Always show drop zone */}
106+
<Paper
107+
onDragOver={handleDragOver}
108+
onDragLeave={handleDragLeave}
109+
onDrop={handleDrop}
110+
onClick={() => fileInputRef.current?.click()}
111+
sx={{
112+
border: `2px dashed ${isDragging ? Colors.purple : Colors.lightGray}`,
113+
borderRadius: 2,
114+
p: 6,
115+
textAlign: "center",
116+
cursor: "pointer",
117+
transition: "all 0.2s",
118+
backgroundColor: isDragging
119+
? "rgba(128, 90, 213, 0.05)"
120+
: "transparent",
121+
"&:hover": {
122+
borderColor: Colors.purple,
123+
backgroundColor: "rgba(128, 90, 213, 0.05)",
124+
},
125+
}}
126+
>
127+
<CloudUpload
128+
sx={{
129+
fontSize: files.length > 0 ? 40 : 64, // ← Smaller icon when files exist
130+
color: Colors.purple,
131+
mb: 1,
132+
}}
133+
/>
134+
<Typography variant={files.length > 0 ? "body1" : "h6"} gutterBottom>
135+
{files.length > 0
136+
? "Drop more files here"
137+
: "Drop your neuroimaging files here"}
138+
</Typography>
139+
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
140+
Supports NIfTI, SNIRF, HDF5, NeuroJSON, folders, and ZIP archives
141+
</Typography>
142+
{files.length === 0 && (
143+
<>
144+
<Typography
145+
variant="caption"
146+
color="text.secondary"
147+
display="block"
148+
sx={{ mb: 2 }}
149+
>
150+
📁 Folders • 🗜️ ZIP files • 📄 Documents (.json, .txt, .md) • 📊
151+
Office (.docx, .pdf, .xlsx)
152+
</Typography>
153+
<Button
154+
variant="outlined"
155+
startIcon={<Add />}
156+
sx={{ borderColor: Colors.purple, color: Colors.purple }}
157+
>
158+
Or Click to Browse
159+
</Button>
160+
</>
161+
)}
162+
<input
163+
ref={fileInputRef}
164+
type="file"
165+
multiple
166+
hidden
167+
onChange={handleFileSelect}
168+
accept=".nii,.nii.gz,.snirf,.h5,.hdf5,.jnii,.jmsh,.json,.txt,.md,.zip,.docx,.pdf,.xlsx,.xls"
169+
/>
170+
</Paper>
171+
</Box>
172+
);
173+
};
174+
175+
export default DropZone;

0 commit comments

Comments
 (0)