Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export const PresetFilesModal = ({ onClose, isOpen, onSubmit, currentSources }:
<div className={styles.formWrapper}>
<div className={styles.uploadForm}>
<h2 className={styles.title}>Upload your files</h2>
<UploadForm files={uploadedFiles} setFiles={setUploadedFiles} accept={'.bin'}/>
<UploadForm files={uploadedFiles} setFiles={setUploadedFiles} allowFolder={false} accept={'.bin'}/>
</div>

<div className={styles.selectFiles}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
}

}
39 changes: 28 additions & 11 deletions front-end/src/components/composite/uploadModal/uploadModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -16,6 +16,7 @@ interface UploadModalProps {
export const UploadModal = ({ isOpen, onClose }: UploadModalProps) => {
const [files, setFiles] = useState<File[]>([]);
const { refetch } = useFiles();
const [allowFolder, setAllowFolder] = useState(false);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this toggle needed here? I like the fact you can disable it, for the presetFilesModal, but for upload I think it should be okay to always allow folders as well as normal files too


const submitFiles = async () => {
const uploadPromises = files.map((file) => ApiUtil.uploadFile(file));
Expand All @@ -28,16 +29,32 @@ export const UploadModal = ({ isOpen, onClose }: UploadModalProps) => {
return (
<BaseModal isOpen={isOpen} onClose={onClose}>
<div className={styles.uploadModal}>
<h2 className={styles.title}>Upload your file</h2>
<UploadForm files={files} setFiles={setFiles} />
<Button
onClick={submitFiles}
textSize={'2rem'}
className={styles.submitButton}
>
<span>Submit</span>
<img src={rightArrowIcon} alt="right arrow"/>
</Button>
<h2 className={styles.title}>Upload your files</h2>

<UploadForm files={files} setFiles={setFiles} allowFolder={true} />

<div className={styles.buttonRow}>

<Button
onClick={() => setAllowFolder(prev => !prev)}
textSize={'2rem'}
className={styles.submitButton}
>
<span>{allowFolder ? 'Folder Upload' : 'File Upload'}</span>
<img src={allowFolder ? folderIcon : cubeIcon} alt='right arrow'/>
</Button>

<Button
onClick={submitFiles}
textSize={'2rem'}
className={styles.submitButton}
>
<span>Submit</span>
<img src={rightArrowIcon} alt="right arrow"/>
</Button>

</div>

</div>
</BaseModal>
);
Expand Down
41 changes: 33 additions & 8 deletions front-end/src/components/ui/uploadForm/UploadForm.module.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@use '@styles/_fonts';
@use '@styles/_variables';

.uploadForm {
@include fonts.text-medium;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
}
Expand Down
99 changes: 84 additions & 15 deletions front-end/src/components/ui/uploadForm/UploadForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLInputElement>(null);
const folderInputRef = React.useRef<HTMLInputElement>(null);

const handleDragOver = (e: React.DragEvent<HTMLLabelElement>) => {
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragging(true);
};

const handleDrop = (e: React.DragEvent<HTMLLabelElement>) => {
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
if (e.dataTransfer.files) {
if(e.dataTransfer.items) {

const newFiles: File[] = [];

const traverseFileTree = (item: FileSystemEntry, path = ''): Promise<void> => {
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<void>[] = [];
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);
Expand All @@ -38,32 +76,63 @@ export const UploadForm = ({ files, setFiles, accept = '.csv, .bin, .mp4, .mov,
setFiles(files.filter((_, i) => i !== index));
};

const handleFolderClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
e.stopPropagation();
folderInputRef.current?.click();
};

const handleContentClick = () => {
fileInputRef.current?.click();
};

return (
<label
<div
className={cx(styles.uploadForm, {[styles.dragover]: isDragging})}
onDragOver={(e) => handleDragOver(e)}
onDragLeave={() => setIsDragging(false)}
onDrop={(e) => handleDrop(e)}
>
<div className={styles.daytimeBg}/>
<div className={styles.nighttimeBg}/>
<img className={styles.nighttimeImage} src={nighttime} alt="nighttime"/>
<img className={styles.daytimeImage} src={daytime} alt="daytime"/>
<img className={styles.nighttimeImage} src={nighttime} alt='nighttime'/>
<img className={styles.daytimeImage} src={daytime} alt='daytime'/>

<div className={cx(styles.uploadFormContent, {
[styles.disabled]: files.length > 0,
[styles.dragover]: isDragging
})}>
<img className={styles.icon} src={uploadIcon} alt="upload icon" />
<p className={styles.text}><strong>Choose a file</strong> or drag it here</p>
<div
className={cx(styles.uploadFormContent, {
[styles.disabled]: files.length > 0,
[styles.dragover]: isDragging
})}
>
<div onClick={handleContentClick} className={styles.clickableArea}>
<img className={styles.icon} src={uploadIcon} alt='upload icon' />
<p className={styles.text}><strong>Choose a file</strong> or drag it here</p>
</div>
{allowFolder && (
<button type="button" className={styles.folderLink} onClick={handleFolderClick}>
or click here for a folder
</button>
)}
<p className={styles.textHover}><strong>Drop the file</strong></p>
<input
ref={fileInputRef}
className={styles.input}
type="file"
type='file'
accept={accept}
multiple={true}
onChange={(e) => {handleFileChange(e);}}
/>
{allowFolder && (
<input
ref={folderInputRef}
className={styles.input}
type='file'
multiple={true}
onChange={(e) => {handleFileChange(e);}}
/* @ts-expect-error – webkitdirectory is a non-standard attribute but required for folder upload */
webkitdirectory=''
/>
)}
</div>
{files.length > 0 && (
<ul className={styles.fileList}>
Expand All @@ -78,6 +147,6 @@ export const UploadForm = ({ files, setFiles, accept = '.csv, .bin, .mp4, .mov,
</ul>
)}

</label>
</div>
);
};