Skip to content

Commit

Permalink
#228 Better dropzone styling + Add file drag & drop to folders
Browse files Browse the repository at this point in the history
  • Loading branch information
Polleps committed Oct 5, 2022
1 parent 1fba675 commit a0a58dd
Show file tree
Hide file tree
Showing 7 changed files with 282 additions and 73 deletions.
104 changes: 104 additions & 0 deletions data-browser/src/components/forms/FileDropzone/FileDropzone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Resource } from '@tomic/react';
import React, { useCallback, useEffect } from 'react';
import { useDropzone } from 'react-dropzone';
import { FaUpload } from 'react-icons/fa';
import styled, { keyframes } from 'styled-components';
import { ErrMessage } from '../InputStyles';
import { useUpload } from './useUpload';

export interface FileDropZoneProps {
parentResource: Resource;
onFilesUploaded?: (files: string[]) => void;
}

/**
* A dropzone for adding files. Renders its children by default, unless you're
* holding a file, an error occurred, or it's uploading.
*/
export function FileDropZone({
parentResource,
children,
onFilesUploaded,
}: React.PropsWithChildren<FileDropZoneProps>): JSX.Element {
const { upload, isUploading, error } = useUpload(parentResource);
const dropzoneRef = React.useRef<HTMLDivElement>(null);
const onDrop = useCallback(
async (files: File[]) => {
const uploaded = await upload(files);
onFilesUploaded?.(uploaded);
},
[upload],
);

const { getRootProps, isDragActive } = useDropzone({ onDrop });

// Move the dropzone down if the user has scrolled down.
useEffect(() => {
if (isDragActive && dropzoneRef.current) {
const rect = dropzoneRef.current.getBoundingClientRect();

if (rect.top < 0) {
dropzoneRef.current.style.top = `calc(${Math.abs(rect.top)}px + 1rem)`;
}
}
}, [isDragActive]);

return (
<Root
{...getRootProps()}
// For some reason this is tabbable by default, but it does not seem to actually help users.
// Let's disable it.
tabIndex={-1}
>
{isUploading && <p>{'Uploading...'}</p>}
{error && <ErrMessage>{error.message}</ErrMessage>}
{children}
{isDragActive && (
<VisualDropzone ref={dropzoneRef}>
<TextWrapper>
<FaUpload /> Drop files here to upload.
</TextWrapper>
</VisualDropzone>
)}
</Root>
);
}

const Root = styled.div`
height: 100%;
position: relative;
`;

const fadeIn = keyframes`
from {
opacity: 0;
backdrop-filter: blur(0px);
}
to {
opacity: 1;
backdrop-filter: blur(10px);
}
`;

const VisualDropzone = styled.div`
position: absolute;
inset: 0;
height: 90vh;
background-color: ${p =>
p.theme.darkMode ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.8)'};
backdrop-filter: blur(10px);
border: 3px dashed ${p => p.theme.colors.textLight};
border-radius: ${p => p.theme.radius};
display: grid;
place-items: center;
font-size: 1.8rem;
color: ${p => p.theme.colors.textLight};
animation: 0.1s ${fadeIn} ease-in;
`;

const TextWrapper = styled.div`
display: flex;
align-items: center;
gap: 1rem;
padding: ${p => p.theme.margin}rem;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Resource } from '@tomic/react';
import React, { useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
import { FaUpload } from 'react-icons/fa';
import styled from 'styled-components';
import { ErrMessage } from '../InputStyles';
import { useUpload } from './useUpload';

export interface FileDropzoneInputProps {
parentResource: Resource;
onFilesUploaded?: (files: string[]) => void;
}

/**
* A dropzone for adding files. Renders its children by default, unless you're
* holding a file, an error occurred, or it's uploading.
*/
export function FileDropzoneInput({
parentResource,
onFilesUploaded,
}: FileDropzoneInputProps): JSX.Element {
const { upload, isUploading, error } = useUpload(parentResource);

const onFileSelect = useCallback(
async (files: File[]) => {
const uploaded = await upload(files);
onFilesUploaded?.(uploaded);
},
[upload],
);

const { getRootProps, getInputProps } = useDropzone({
onDrop: onFileSelect,
});

return (
<>
<VisualDropZone {...getRootProps()}>
{error && <ErrMessage>{error.message}</ErrMessage>}
<input {...getInputProps()} />
<TextWrapper>
<FaUpload />{' '}
{isUploading ? 'Uploading...' : 'Drop files or click here to upload.'}
</TextWrapper>
</VisualDropZone>
</>
);
}

const VisualDropZone = styled.div`
background-color: ${p =>
p.theme.darkMode ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.8)'};
backdrop-filter: blur(10px);
border: 2px dashed ${p => p.theme.colors.bg2};
border-radius: ${p => p.theme.radius};
display: grid;
place-items: center;
font-size: 1.3rem;
color: ${p => p.theme.colors.textLight};
min-height: 10rem;
cursor: pointer;
&:hover,
&focus {
color: ${p => p.theme.colors.main};
border-color: ${p => p.theme.colors.main};
}
`;

const TextWrapper = styled.div`
display: flex;
align-items: center;
padding: ${p => p.theme.margin}rem;
gap: 1rem;
`;
56 changes: 56 additions & 0 deletions data-browser/src/components/forms/FileDropzone/useUpload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {
properties,
Resource,
uploadFiles,
useArray,
useStore,
} from '@tomic/react';
import { useCallback, useState } from 'react';

export interface UseUploadResult {
/** Uploads files to the upload endpoint and returns the created subjects. */
upload: (acceptedFiles: File[]) => Promise<string[]>;
isUploading: boolean;
error: Error | undefined;
}

export function useUpload(parentResource: Resource): UseUploadResult {
const store = useStore();
const [isUploading, setIsUploading] = useState(false);
const [error, setError] = useState<Error | undefined>(undefined);
const [subResources, setSubResources] = useArray(
parentResource,
properties.subResources,
);

const upload = useCallback(
async (acceptedFiles: File[]) => {
try {
setError(undefined);
setIsUploading(true);
const netUploaded = await uploadFiles(
acceptedFiles,
store,
parentResource.getSubject(),
);
const allUploaded = [...netUploaded];
setIsUploading(false);
setSubResources([...subResources, ...allUploaded]);

return allUploaded;
} catch (e) {
setError(e);
setIsUploading(false);

return [];
}
},
[parentResource],
);

return {
upload,
isUploading,
error,
};
}
62 changes: 0 additions & 62 deletions data-browser/src/components/forms/UploadForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import React, { useCallback, useState } from 'react';
import { Resource, uploadFiles } from '@tomic/react';
import { useStore } from '@tomic/react';
import { useDropzone } from 'react-dropzone';
import styled from 'styled-components';

import { Button } from '../Button';
import FilePill from '../FilePill';
import { ErrMessage } from './InputStyles';
Expand Down Expand Up @@ -77,63 +75,3 @@ export default function UploadForm({
</div>
);
}

interface UploadWrapperProps extends UploadFormProps {
children: React.ReactNode;
onFilesUploaded: (filesSubjects: string[]) => unknown;
}

/**
* A dropzone for adding files. Renders its children by default, unless you're
* holding a file, an error occurred, or it's uploading.
*/
export function UploadWrapper({
parentResource,
children,
onFilesUploaded,
}: UploadWrapperProps) {
const store = useStore();
// const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
const [isUploading, setIsUploading] = useState(false);
const [err, setErr] = useState<Error | undefined>(undefined);
const onDrop = useCallback(
async (acceptedFiles: File[]) => {
try {
setErr(undefined);
setIsUploading(true);
const netUploaded = await uploadFiles(
acceptedFiles,
store,
parentResource.getSubject(),
);
const allUploaded = [...netUploaded];
onFilesUploaded(allUploaded);
setIsUploading(false);
} catch (e) {
setErr(e);
setIsUploading(false);
}
},
[onFilesUploaded],
);
const { getRootProps, isDragActive } = useDropzone({ onDrop });

return (
<div
{...getRootProps()}
// For some reason this is tabbable by default, but it does not seem to actually help users.
// Let's disable it.
tabIndex={-1}
>
{isUploading && <p>{'Uploading...'}</p>}
{err && <ErrMessage>{err.message}</ErrMessage>}
{isDragActive ? <Fill>{'Drop the files here ...'}</Fill> : children}
</div>
);
}

const Fill = styled.div`
height: 100%;
width: 100%;
min-height: 4rem;
`;
26 changes: 24 additions & 2 deletions data-browser/src/routes/NewRoute.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { useResource, useString } from '@tomic/react';
import { urls } from '@tomic/react';
import React, { useState } from 'react';
import React, { useCallback, useState } from 'react';
import { useNavigate } from 'react-router';

import { newURL, useQueryString } from '../helpers/navigation';
import {
constructOpenURL,
newURL,
useQueryString,
} from '../helpers/navigation';
import { ContainerNarrow } from '../components/Containers';
import NewIntanceButton from '../components/NewInstanceButton';
import { ResourceSelector } from '../components/forms/ResourceSelector';
Expand All @@ -13,6 +17,8 @@ import { Row } from '../components/Row';
import { NewFormFullPage } from '../components/forms/NewForm/index';
import { ResourceInline } from '../views/ResourceInline';
import styled from 'styled-components';
import { FileDropzoneInput } from '../components/forms/FileDropzone/FileDropzoneInput';
import toast from 'react-hot-toast';

/** Start page for instantiating a new Resource from some Class */
function New(): JSX.Element {
Expand All @@ -27,6 +33,7 @@ function New(): JSX.Element {
const { drive } = useSettings();

const calculatedParent = parentSubject || drive;
const parentResource = useResource(calculatedParent);

function handleClassSet(e) {
if (!classInput) {
Expand All @@ -39,6 +46,17 @@ function New(): JSX.Element {
navigate(newURL(classInput, calculatedParent));
}

const onUploadComplete = useCallback(
(files: string[]) => {
toast.success(`Uploaded ${files.length} files.`);

if (parentSubject) {
navigate(constructOpenURL(parentSubject));
}
},
[parentSubject, navigate],
);

return (
<ContainerNarrow>
{classSubject ? (
Expand Down Expand Up @@ -107,6 +125,10 @@ function New(): JSX.Element {
</>
)}
</Row>
<FileDropzoneInput
parentResource={parentResource}
onFilesUploaded={onUploadComplete}
/>
</StyledForm>
)}
</ContainerNarrow>
Expand Down
Loading

0 comments on commit a0a58dd

Please sign in to comment.