client: drag and drop file uploading (#20)
This commit is contained in:
parent
4bdd5435ce
commit
54adafa41d
8 changed files with 251 additions and 3 deletions
|
@ -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);
|
||||
}
|
178
client/components/new-post/drag-and-drop/index.tsx
Normal file
178
client/components/new-post/drag-and-drop/index.tsx
Normal 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
|
|
@ -3,9 +3,11 @@ import { useRouter } from 'next/router';
|
|||
import { useCallback, useState } from 'react'
|
||||
import generateUUID from '../../lib/generate-uuid';
|
||||
import Document from '../document';
|
||||
import FileDropzone from './drag-and-drop';
|
||||
import styles from './post.module.css'
|
||||
import Title from './title';
|
||||
type Document = {
|
||||
|
||||
export type Document = {
|
||||
title: string
|
||||
content: string
|
||||
id: string
|
||||
|
@ -63,6 +65,7 @@ const Post = () => {
|
|||
return (
|
||||
<div>
|
||||
<Title title={title} setTitle={setTitle} />
|
||||
<FileDropzone docs={docs} setDocs={setDocs} />
|
||||
{
|
||||
docs.map(({ id }) => {
|
||||
const doc = docs.find((doc) => doc.id === id)
|
|
@ -19,6 +19,7 @@
|
|||
"react": "17.0.2",
|
||||
"react-debounce-render": "^8.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-dropzone": "^12.0.4",
|
||||
"react-loading-skeleton": "^3.0.3",
|
||||
"react-markdown": "^8.0.0",
|
||||
"react-syntax-highlighter": "^15.4.5",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Head from 'next/head'
|
||||
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 useSignedIn from '../lib/hooks/use-signed-in'
|
||||
import Header from '../components/header'
|
||||
|
@ -24,7 +24,7 @@ const Home = ({ theme, changeTheme }: ThemeProps) => {
|
|||
</Page.Header>
|
||||
|
||||
<Page.Content paddingTop={"var(--gap)"} width={"var(--main-content-width)"} margin="0 auto" className={styles.main}>
|
||||
{isSignedIn && <HomeComponent />}
|
||||
{isSignedIn && <NewPost />}
|
||||
</Page.Content>
|
||||
</Page >
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
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:
|
||||
version "4.4.1"
|
||||
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:
|
||||
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:
|
||||
version "7.0.1"
|
||||
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"
|
||||
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:
|
||||
version "16.13.1"
|
||||
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"
|
||||
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:
|
||||
version "3.21.0"
|
||||
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
|
||||
|
|
Loading…
Reference in a new issue