client: drag and drop file uploading

This commit is contained in:
Max Leiter 2022-03-11 15:17:00 -08:00
parent 4bdd5435ce
commit 9a506bd9da
No known key found for this signature in database
GPG key ID: A3512F2F2F17EBDA
8 changed files with 251 additions and 3 deletions

View file

@ -0,0 +1,40 @@
.container {
display: flex;
flex-direction: column;
}
.container ul {
margin: 0;
margin-top: var(--gap-double);
}
.dropzone {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
border-width: 2px;
border-radius: 2px;
border-style: dashed;
outline: none;
transition: border 0.24s ease-in-out;
}
.error {
color: red;
font-size: 0.8rem;
transition: border 0.24s ease-in-out;
border: 2px solid red;
border-radius: 2px;
padding: 20px;
}
.error > li:before {
content: "";
}
.error ul {
margin: 0;
padding-left: var(--gap-double);
}

View file

@ -0,0 +1,178 @@
import { Button, Text, useTheme, useToasts } from '@geist-ui/core'
import { useCallback, useEffect } from 'react'
import { useDropzone } from 'react-dropzone'
import styles from './drag-and-drop.module.css'
import { Document } from '../'
import generateUUID from '../../../lib/generate-uuid'
import { XCircle } from '@geist-ui/icons'
const allowedFileTypes = [
'application/json',
'application/x-javascript',
'application/xhtml+xml',
'application/xml',
'text/xml',
'text/plain',
'text/html',
'text/csv',
'text/tab-separated-values',
'text/x-c',
'text/x-c++',
'text/x-csharp',
'text/x-java',
'text/x-javascript',
'text/x-php',
'text/x-python',
'text/x-ruby',
'text/x-scala',
'text/x-swift',
'text/x-typescript',
'text/x-vb',
'text/x-vbscript',
'text/x-yaml',
'text/x-c++',
'text/x-c#',
'text/mathml',
'text/x-markdown',
'text/markdown',
]
// Files with no extension can't be easily detected as plain-text,
// so instead of allowing all of them we'll just allow common ones
const allowedFileNames = [
'Makefile',
'README',
'Dockerfile',
'Jenkinsfile',
'LICENSE',
'.env',
'.gitignore',
'.gitattributes',
'.env.example',
'.env.development',
'.env.production',
'.env.test',
'.env.staging',
'.env.development.local',
'yarn.lock',
]
const allowedFileExtensions = [
'json',
'js',
'jsx',
'ts',
'tsx',
'c',
'cpp',
'c++',
'c#',
'java',
'php',
'py',
'rb',
'scala',
'swift',
'vb',
'vbscript',
'yaml',
'less',
'stylus',
'styl',
'sass',
'scss',
'lock',
'md',
'markdown',
'txt',
'html',
'htm',
'css',
'csv',
'log',
'sql',
'xml',
'webmanifest',
]
// TODO: this shouldn't need to know about docs
function FileDropzone({ setDocs, docs }: { setDocs: (docs: Document[]) => void, docs: Document[] }) {
const { palette } = useTheme()
const onDrop = useCallback((acceptedFiles) => {
acceptedFiles.forEach((file: File) => {
const reader = new FileReader()
reader.onabort = () => console.log('file reading was aborted')
reader.onerror = () => console.log('file reading has failed')
reader.onload = () => {
const content = reader.result as string
if (docs.length === 1 && docs[0].content === '') {
setDocs([{
title: file.name,
content,
id: generateUUID()
}])
} else {
setDocs([...docs, {
title: file.name,
content,
id: generateUUID()
}])
}
}
reader.readAsText(file)
})
}, [docs, setDocs])
const validator = (file: File) => {
// TODO: make this configurable
const maxFileSize = 1000000;
if (file.size > maxFileSize) {
return {
code: 'file-too-big',
message: 'File is too big. Maximum file size is ' + (maxFileSize).toFixed(2) + ' MB.',
}
}
// We initially try to use the browser provided mime type, and then fall back to file names and finally extensions
if (allowedFileTypes.includes(file.type) || allowedFileNames.includes(file.name) || allowedFileExtensions.includes(file.name?.split('.').pop() || '')) {
return null
} else {
return {
code: "not-plain-text",
message: `Only plain text files are allowed.`
};
}
}
const { getRootProps, getInputProps, isDragActive, fileRejections } = useDropzone({ onDrop, validator })
const fileRejectionItems = fileRejections.map(({ file, errors }) => (
<li key={file.name}>
{file.name}:
<ul>
{errors.map(e => (
<li key={e.code}><Text>{e.message}</Text></li>
))}
</ul>
</li>
));
return (
<div className={styles.container}>
<div {...getRootProps()} className={styles.dropzone} style={{
borderColor: palette.accents_3,
}}>
<input {...getInputProps()} />
{!isDragActive && <Text p>Drag some files here, or click to select files</Text>}
{isDragActive && <Text p>Release to drop the files here</Text>}
</div>
{fileRejections.length > 0 && <ul className={styles.error}>
{/* <Button style={{ float: 'right' }} type="abort" onClick={() => fileRejections.splice(0, fileRejections.length)} auto iconRight={<XCircle />}></Button> */}
<Text h5>There was a problem with some of your files.</Text>
{fileRejectionItems}
</ul>}
</div>
)
}
export default FileDropzone

View file

@ -3,9 +3,11 @@ import { useRouter } from 'next/router';
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import generateUUID from '../../lib/generate-uuid'; import generateUUID from '../../lib/generate-uuid';
import Document from '../document'; import Document from '../document';
import FileDropzone from './drag-and-drop';
import styles from './post.module.css' import styles from './post.module.css'
import Title from './title'; import Title from './title';
type Document = {
export type Document = {
title: string title: string
content: string content: string
id: string id: string
@ -63,6 +65,7 @@ const Post = () => {
return ( return (
<div> <div>
<Title title={title} setTitle={setTitle} /> <Title title={title} setTitle={setTitle} />
<FileDropzone docs={docs} setDocs={setDocs} />
{ {
docs.map(({ id }) => { docs.map(({ id }) => {
const doc = docs.find((doc) => doc.id === id) const doc = docs.find((doc) => doc.id === id)

View file

@ -19,6 +19,7 @@
"react": "17.0.2", "react": "17.0.2",
"react-debounce-render": "^8.0.2", "react-debounce-render": "^8.0.2",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-dropzone": "^12.0.4",
"react-loading-skeleton": "^3.0.3", "react-loading-skeleton": "^3.0.3",
"react-markdown": "^8.0.0", "react-markdown": "^8.0.0",
"react-syntax-highlighter": "^15.4.5", "react-syntax-highlighter": "^15.4.5",

View file

@ -1,6 +1,6 @@
import Head from 'next/head' import Head from 'next/head'
import styles from '../styles/Home.module.css' import styles from '../styles/Home.module.css'
import HomeComponent from '../components/post' import NewPost from '../components/new-post'
import { Page } from '@geist-ui/core' import { Page } from '@geist-ui/core'
import useSignedIn from '../lib/hooks/use-signed-in' import useSignedIn from '../lib/hooks/use-signed-in'
import Header from '../components/header' import Header from '../components/header'
@ -24,7 +24,7 @@ const Home = ({ theme, changeTheme }: ThemeProps) => {
</Page.Header> </Page.Header>
<Page.Content paddingTop={"var(--gap)"} width={"var(--main-content-width)"} margin="0 auto" className={styles.main}> <Page.Content paddingTop={"var(--gap)"} width={"var(--main-content-width)"} margin="0 auto" className={styles.main}>
{isSignedIn && <HomeComponent />} {isSignedIn && <NewPost />}
</Page.Content> </Page.Content>
</Page > </Page >
) )

View file

@ -371,6 +371,11 @@ ast-types-flow@^0.0.7:
resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad"
integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0= integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0=
attr-accept@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b"
integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==
axe-core@^4.3.5: axe-core@^4.3.5:
version "4.4.1" version "4.4.1"
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.1.tgz#7dbdc25989298f9ad006645cd396782443757413" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.1.tgz#7dbdc25989298f9ad006645cd396782443757413"
@ -935,6 +940,13 @@ file-entry-cache@^6.0.1:
dependencies: dependencies:
flat-cache "^3.0.4" flat-cache "^3.0.4"
file-selector@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.4.0.tgz#59ec4f27aa5baf0841e9c6385c8386bef4d18b17"
integrity sha512-iACCiXeMYOvZqlF1kTiYINzgepRBymz1wwjiuup9u9nayhb6g4fSwiyJ/6adli+EPwrWtpgQAh2PoS7HukEGEg==
dependencies:
tslib "^2.0.3"
fill-range@^7.0.1: fill-range@^7.0.1:
version "7.0.1" version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
@ -2253,6 +2265,15 @@ react-dom@17.0.2:
object-assign "^4.1.1" object-assign "^4.1.1"
scheduler "^0.20.2" scheduler "^0.20.2"
react-dropzone@^12.0.4:
version "12.0.4"
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-12.0.4.tgz#b88eeaa2c7118f7fd042404682b17a1d466f2fcf"
integrity sha512-fcqHEYe1MzAghU6/Hz86lHDlBNsA+lO48nAcm7/wA+kIzwS6uuJbUG33tBZjksj7GAZ1iUQ6NHwjUURPmSGang==
dependencies:
attr-accept "^2.2.2"
file-selector "^0.4.0"
prop-types "^15.8.1"
react-is@^16.13.1, react-is@^16.7.0: react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@ -2670,6 +2691,11 @@ tslib@^1.8.1:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.0.3:
version "2.3.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
tsutils@^3.21.0: tsutils@^3.21.0:
version "3.21.0" version "3.21.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"