diff --git a/front-end/src/components/composite/uploadModal/uploadModal.module.scss b/front-end/src/components/composite/uploadModal/uploadModal.module.scss
index fdf3856d..ec71bd8e 100644
--- a/front-end/src/components/composite/uploadModal/uploadModal.module.scss
+++ b/front-end/src/components/composite/uploadModal/uploadModal.module.scss
@@ -16,4 +16,11 @@
.submitButton {
margin-top: 1.25rem;
}
+.buttonRow {
+ display: flex;
+ gap: 1rem; /* space between buttons */
+ justify-content: flex-start; /* or center, or flex-end */
+ margin-top: 1rem; /* optional spacing from form */
+}
+
}
\ No newline at end of file
diff --git a/front-end/src/components/composite/uploadModal/uploadModal.tsx b/front-end/src/components/composite/uploadModal/uploadModal.tsx
index cb03b63a..960612e5 100644
--- a/front-end/src/components/composite/uploadModal/uploadModal.tsx
+++ b/front-end/src/components/composite/uploadModal/uploadModal.tsx
@@ -3,7 +3,7 @@ import { BaseModal } from '@components/ui/baseModal/BaseModal';
import { UploadForm } from '@components/ui/uploadForm/UploadForm';
import { Button } from '@components/ui/button/Button';
import { useState } from 'react';
-import {rightArrowIcon} from '@assets/icons';
+import {rightArrowIcon, folderIcon, cubeIcon} from '@assets/icons';
import { ApiUtil } from '@lib/apiUtils';
import { showSuccessToast } from '@components/ui/toastNotification/ToastNotification';
import { useFiles } from '@lib/files/useFiles';
@@ -16,6 +16,7 @@ interface UploadModalProps {
export const UploadModal = ({ isOpen, onClose }: UploadModalProps) => {
const [files, setFiles] = useState
([]);
const { refetch } = useFiles();
+ const [allowFolder, setAllowFolder] = useState(false);
const submitFiles = async () => {
const uploadPromises = files.map((file) => ApiUtil.uploadFile(file));
@@ -28,16 +29,32 @@ export const UploadModal = ({ isOpen, onClose }: UploadModalProps) => {
return (
-
Upload your file
-
-
+
Upload your files
+
+
+
+
+
+
+
+
+
+
+
);
diff --git a/front-end/src/components/ui/uploadForm/UploadForm.module.scss b/front-end/src/components/ui/uploadForm/UploadForm.module.scss
index b6421d58..d6f64a99 100644
--- a/front-end/src/components/ui/uploadForm/UploadForm.module.scss
+++ b/front-end/src/components/ui/uploadForm/UploadForm.module.scss
@@ -1,4 +1,5 @@
@use '@styles/_fonts';
+@use '@styles/_variables';
.uploadForm {
@include fonts.text-medium;
@@ -15,14 +16,6 @@
border-radius: 10px;
overflow: hidden;
- &:hover {
- cursor: pointer;
- .uploadFormContent {
- transform: scale(1.05);
- filter: brightness(0.9);
- }
- }
-
&.dragover {
.daytimeBg, .daytimeImage {
opacity: 0;
@@ -85,6 +78,12 @@
padding: 1.75rem 4rem;
width: fit-content;
transition: transform 0.3s ease, filter 0.3s ease, border-color 0.3s ease;
+ cursor: pointer;
+
+ &:hover {
+ transform: scale(1.05);
+ filter: brightness(0.9);
+ }
&.disabled {
display: none;
@@ -101,6 +100,14 @@
opacity: 0.8;
}
+ .clickableArea {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1.25rem;
+ cursor: pointer;
+ }
+
.text, .textHover {
margin: 0;
transition: opacity 0.3s ease;
@@ -111,6 +118,24 @@
margin-top: -2.75rem;
}
+ .folderLink {
+ all: unset;
+ color: var(--text-colour);
+ opacity: 0.7;
+ font-size: 0.875rem;
+ text-decoration: underline;
+ text-underline-offset: 0.2rem;
+ margin-top: -0.75rem;
+ transition: opacity 0.3s ease;
+ cursor: pointer;
+ position: relative;
+ z-index: map-get(variables.$z-index, 'tooltip');
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+
.input {
display: none;
}
diff --git a/front-end/src/components/ui/uploadForm/UploadForm.tsx b/front-end/src/components/ui/uploadForm/UploadForm.tsx
index 7bb979cf..1fd7b0fd 100644
--- a/front-end/src/components/ui/uploadForm/UploadForm.tsx
+++ b/front-end/src/components/ui/uploadForm/UploadForm.tsx
@@ -9,19 +9,57 @@ interface UploadFormProps {
files: File[];
setFiles: (files: File[]) => void;
accept?: string;
+ allowFolder: boolean;
}
-export const UploadForm = ({ files, setFiles, accept = '.csv, .bin, .mp4, .mov, .fit' }: UploadFormProps) => {
+
+export const UploadForm = ({ files, setFiles, allowFolder = false,
+ accept = '.csv, .bin, .mp4, .mov, .fit' }:UploadFormProps) => {
+
const [isDragging, setIsDragging] = React.useState(false);
+ const fileInputRef = React.useRef(null);
+ const folderInputRef = React.useRef(null);
- const handleDragOver = (e: React.DragEvent) => {
+ const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(true);
};
- const handleDrop = (e: React.DragEvent) => {
+ const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
- if (e.dataTransfer.files) {
+ if(e.dataTransfer.items) {
+
+ const newFiles: File[] = [];
+
+ const traverseFileTree = (item: FileSystemEntry, path = ''): Promise => {
+ return new Promise((resolve) => {
+ if (item.isFile) {
+ (item as FileSystemFileEntry).file((file: File) => {
+ const f = new File([file], file.name, { type: file.type });
+ newFiles.push(f);
+ resolve();
+ });
+ } else if (item.isDirectory) {
+ const dirReader = (item as FileSystemDirectoryEntry).createReader();
+ dirReader.readEntries((entries: FileSystemEntry[]) => {
+ Promise.all(entries.map((entry) => traverseFileTree(entry, path+item.name + '/'))).then(() => resolve());
+ });
+ } else {
+ resolve();
+ }
+ });
+ };
+
+ const promises: Promise[] = [];
+ for (const item of Array.from(e.dataTransfer.items)) {
+ const entry = item.webkitGetAsEntry();
+ if (entry) promises.push(traverseFileTree(entry));
+ }
+
+ Promise.all(promises).then(() => {
+ setFiles([...files, ...newFiles]);
+ });
+ } else if (e.dataTransfer.files) {
setFiles([...files, ...Array.from(e.dataTransfer.files)]);
}
setIsDragging(false);
@@ -38,8 +76,18 @@ export const UploadForm = ({ files, setFiles, accept = '.csv, .bin, .mp4, .mov,
setFiles(files.filter((_, i) => i !== index));
};
+ const handleFolderClick = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ folderInputRef.current?.click();
+ };
+
+ const handleContentClick = () => {
+ fileInputRef.current?.click();
+ };
+
return (
-