diff --git a/client/components/new-post/drag-and-drop/drag-and-drop.module.css b/client/components/new-post/drag-and-drop/drag-and-drop.module.css
new file mode 100644
index 00000000..c9b16951
--- /dev/null
+++ b/client/components/new-post/drag-and-drop/drag-and-drop.module.css
@@ -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);
+}
diff --git a/client/components/new-post/drag-and-drop/index.tsx b/client/components/new-post/drag-and-drop/index.tsx
new file mode 100644
index 00000000..9e5eeab4
--- /dev/null
+++ b/client/components/new-post/drag-and-drop/index.tsx
@@ -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 }) => (
+
+ {file.name}:
+
+ {errors.map(e => (
+ - {e.message}
+ ))}
+
+
+ ));
+
+ return (
+
+
+
+ {!isDragActive && Drag some files here, or click to select files}
+ {isDragActive && Release to drop the files here}
+
+ {fileRejections.length > 0 &&
+ {/* */}
+ There was a problem with some of your files.
+ {fileRejectionItems}
+
}
+
+ )
+}
+
+export default FileDropzone
\ No newline at end of file
diff --git a/client/components/post/index.tsx b/client/components/new-post/index.tsx
similarity index 96%
rename from client/components/post/index.tsx
rename to client/components/new-post/index.tsx
index b9ba49fe..aaaa1cf5 100644
--- a/client/components/post/index.tsx
+++ b/client/components/new-post/index.tsx
@@ -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 (
+
{
docs.map(({ id }) => {
const doc = docs.find((doc) => doc.id === id)
diff --git a/client/components/post/post.module.css b/client/components/new-post/post.module.css
similarity index 100%
rename from client/components/post/post.module.css
rename to client/components/new-post/post.module.css
diff --git a/client/components/post/title/index.tsx b/client/components/new-post/title/index.tsx
similarity index 100%
rename from client/components/post/title/index.tsx
rename to client/components/new-post/title/index.tsx
diff --git a/client/package.json b/client/package.json
index a3a3fdd4..71d47a7a 100644
--- a/client/package.json
+++ b/client/package.json
@@ -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",
diff --git a/client/pages/new.tsx b/client/pages/new.tsx
index d05e8b9d..8a7f0b63 100644
--- a/client/pages/new.tsx
+++ b/client/pages/new.tsx
@@ -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) => {
- {isSignedIn && }
+ {isSignedIn && }
)
diff --git a/client/yarn.lock b/client/yarn.lock
index 50e23b31..336c1760 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -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"