dropzone component

This commit is contained in:
Matthias Kretschmann 2019-09-09 13:53:06 +02:00
parent a258f6b94b
commit a2a6720fd8
Signed by: m
GPG Key ID: 606EEEF3C479A91F
11 changed files with 223 additions and 53 deletions

View File

@ -3870,6 +3870,21 @@
"resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz",
"integrity": "sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY="
},
"attr-accept": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-1.1.3.tgz",
"integrity": "sha512-iT40nudw8zmCweivz6j58g+RT33I4KbaIvRUhjNmDwO2WmsQUxFEZZYZ5w3vXe5x5MX9D7mfvA/XaLOZYFR9EQ==",
"requires": {
"core-js": "^2.5.0"
},
"dependencies": {
"core-js": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz",
"integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A=="
}
}
},
"autoprefixer": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.6.1.tgz",
@ -8657,6 +8672,14 @@
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.2.tgz",
"integrity": "sha512-Wz3c3XQ5xroCxd1G8b7yL0Ehkf0TC9oYC6buPFkNnU9EnaPlifeAFCyCh+iewXTyFRcg0a6j3J7FmJsIhlhBdw=="
},
"file-selector": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.1.12.tgz",
"integrity": "sha512-Kx7RTzxyQipHuiqyZGf+Nz4vY9R1XGxuQl/hLoJwq+J4avk/9wxxgZyHKtbyIPJmbD4A66DWGYfyykWNpcYutQ==",
"requires": {
"tslib": "^1.9.0"
}
},
"file-type": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz",
@ -20906,6 +20929,16 @@
"object.pick": "^1.3.0"
}
},
"react-dropzone": {
"version": "10.1.8",
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-10.1.8.tgz",
"integrity": "sha512-Lm6+TxIDf/my4i3VdYmufRcrJ4SUbSTJP3HB49V2+HNjZwLI4NKVkaNRHwwSm9CEuzMP+6SW7pT1txc1uBPfDg==",
"requires": {
"attr-accept": "^1.1.3",
"file-selector": "^0.1.11",
"prop-types": "^15.7.2"
}
},
"react-error-overlay": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.1.tgz",
@ -24177,8 +24210,7 @@
"tslib": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==",
"dev": true
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ=="
},
"tsutils": {
"version": "3.17.1",

View File

@ -33,6 +33,7 @@
"react-datepicker": "^2.8.0",
"react-dom": "16.8.6",
"react-dotdotdot": "^1.3.1",
"react-dropzone": "^10.1.8",
"react-ga": "^2.6.0",
"react-helmet": "^5.2.1",
"react-markdown": "^4.1.0",

View File

@ -0,0 +1,36 @@
@import '../../styles/variables';
.dropzone {
margin-top: $spacer;
margin-bottom: $spacer;
border: .2rem dashed $brand-grey-lighter;
border-radius: 1rem;
padding: $spacer;
background: $brand-white;
transition: .2s ease-out;
cursor: pointer;
p {
text-align: center;
margin-bottom: 0;
font-size: $font-size-small;
color: $brand-grey-light;
}
&:focus,
&:active {
border-color: $brand-grey-light;
outline: 0;
}
}
.dragover {
composes: dropzone;
border-color: $brand-pink;
}
.disabled {
composes: dropzone;
opacity: .5;
pointer-events: none;
}

View File

@ -0,0 +1,52 @@
import React from 'react'
import { fireEvent, render } from '@testing-library/react'
import Dropzone from './Dropzone'
function mockData(files: any) {
return {
dataTransfer: {
files,
items: files.map((file: any) => ({
kind: 'file',
type: file.type,
getAsFile: () => file
})),
types: ['Files']
}
}
}
function flushPromises(ui: any, container: any) {
return new Promise(resolve =>
setImmediate(() => {
render(ui, { container })
resolve(container)
})
)
}
function dispatchEvt(node: any, type: string, data: any) {
const event = new Event(type, { bubbles: true })
Object.assign(event, data)
fireEvent(node, event)
}
test('invoke onDragEnter when dragenter event occurs', async () => {
const file = new File([JSON.stringify({ ping: true })], 'ping.json', {
type: 'application/json'
})
const data = mockData([file])
const handleOnDrop = jest.fn()
const ui = <Dropzone handleOnDrop={handleOnDrop} />
const { container } = render(ui)
// drop a file
const dropzone = container.querySelector('div')
dispatchEvt(dropzone, 'dragenter', data)
dispatchEvt(dropzone, 'dragover', data)
dispatchEvt(dropzone, 'drop', data)
await flushPromises(ui, container)
expect(handleOnDrop).toHaveBeenCalled()
})

View File

@ -0,0 +1,34 @@
import React, { useCallback } from 'react'
import { useDropzone } from 'react-dropzone'
import styles from './Dropzone.module.scss'
export default function Dropzone({
handleOnDrop,
disabled
}: {
handleOnDrop(files: FileList): void
disabled?: boolean
}) {
const onDrop = useCallback(acceptedFiles => handleOnDrop(acceptedFiles), [
handleOnDrop
])
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop
})
return (
<div
{...getRootProps({
className: isDragActive
? styles.dragover
: disabled
? styles.disabled
: styles.dropzone
})}
>
<input {...getInputProps()} />
<p>{`Drag 'n' drop some files here, or click to select files`}</p>
</div>
)
}

View File

@ -7,19 +7,21 @@ let ipfs: any = null
let ipfsMessage = ''
let ipfsVersion = ''
export default function useIpfsApi(config: {
interface IpfsConfig {
host: string
port: string
protocol: string
}) {
}
export default function useIpfsApi(config: IpfsConfig) {
const [isIpfsReady, setIpfsReady] = useState(Boolean(ipfs))
const [ipfsError, setIpfsError] = useState(null)
useEffect(() => {
async function initIpfs() {
ipfsMessage = 'Checking IPFS gateway...'
if (ipfs !== null) return
if (ipfs) return
ipfsMessage = 'Checking IPFS gateway...'
try {
const message = 'Connected to IPFS gateway'
@ -35,8 +37,11 @@ export default function useIpfsApi(config: {
}
setIpfsReady(Boolean(ipfs))
}
initIpfs()
initIpfs()
}, [config])
useEffect(() => {
// just like componentWillUnmount()
return function cleanup() {
if (ipfs) {
@ -47,7 +52,7 @@ export default function useIpfsApi(config: {
setIpfsError(null)
}
}
}, [config])
}, [])
return { ipfs, ipfsVersion, isIpfsReady, ipfsError, ipfsMessage }
}

View File

@ -1,4 +1,4 @@
@import '../../../styles/variables';
@import '../../../../styles/variables';
.ipfsForm {
margin-top: $spacer / 2;

View File

@ -1,12 +1,14 @@
import React from 'react'
import { render } from '@testing-library/react'
import Ipfs from './Ipfs'
import Ipfs from '.'
const addFile = jest.fn()
describe('Ipfs', () => {
const ui = <Ipfs addFile={addFile} />
it('renders without crashing', async () => {
const { container, findByText } = render(<Ipfs addFile={addFile} />)
const { container, findByText } = render(ui)
expect(container.firstChild).toBeInTheDocument()
// wait for IPFS node

View File

@ -1,33 +1,12 @@
/* eslint-disable no-console */
import React, { useState } from 'react'
import axios from 'axios'
import useIpfsApi from '../../../hooks/use-ipfs-api'
import Label from '../../../components/atoms/Form/Label'
import Spinner from '../../../components/atoms/Spinner'
import styles from './Ipfs.module.scss'
async function pingUrl(url: string) {
try {
const response = await axios(url)
if (response.status !== 200) console.error(`Not found: ${url}`)
console.log(`File found: ${url}`)
return
} catch (error) {
console.error(error.message)
}
}
function formatBytes(a: number, b: number) {
if (a === 0) return '0 Bytes'
const c = 1024
const d = b || 2
const e = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const f = Math.floor(Math.log(a) / Math.log(c))
return parseFloat((a / Math.pow(c, f)).toFixed(d)) + ' ' + e[f]
}
import useIpfsApi from '../../../../hooks/use-ipfs-api'
import Label from '../../../../components/atoms/Form/Label'
import Spinner from '../../../../components/atoms/Spinner'
import Dropzone from '../../../../components/molecules/Dropzone'
import { formatBytes, pingUrl } from './utils'
import styles from './index.module.scss'
export default function Ipfs({ addFile }: { addFile(url: string): void }) {
const config = {
@ -80,15 +59,17 @@ export default function Ipfs({ addFile }: { addFile(url: string): void }) {
}
}
function handleCaptureFile(files: FileList | null) {
const reader: any = new window.FileReader()
const file = files && files[0]
function handleOnDrop(files: any) {
const reader: any = new FileReader()
reader.readAsArrayBuffer(file)
reader.onloadend = () => {
const buffer: any = Buffer.from(reader.result)
saveToIpfs(buffer)
}
files &&
files.forEach((file: File) => {
reader.readAsArrayBuffer(file)
reader.onloadend = () => {
const buffer: any = Buffer.from(reader.result)
saveToIpfs(buffer)
}
})
}
return (
@ -99,13 +80,7 @@ export default function Ipfs({ addFile }: { addFile(url: string): void }) {
{loading ? (
<Spinner message={message} />
) : (
<input
type="file"
name="fileUpload"
id="fileUpload"
onChange={e => handleCaptureFile(e.target.files)}
disabled={!isIpfsReady}
/>
<Dropzone handleOnDrop={handleOnDrop} disabled={!isIpfsReady} />
)}
{ipfsMessage !== '' && (
<div className={styles.message} title={ipfsVersion}>

View File

@ -0,0 +1,9 @@
import { formatBytes } from './utils'
describe('utils', () => {
it('formatBytes outputs as expected', () => {
const number = 1024
const output = formatBytes(number, 0)
expect(output).toBe('1 KB')
})
})

View File

@ -0,0 +1,24 @@
/* eslint-disable no-console */
import axios from 'axios'
export async function pingUrl(url: string) {
try {
const response = await axios(url)
if (response.status !== 200) console.error(`Not found: ${url}`)
console.log(`File found: ${url}`)
return
} catch (error) {
console.error(error.message)
}
}
export function formatBytes(a: number, b: number) {
if (a === 0) return '0 Bytes'
const c = 1024
const d = b || 2
const e = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const f = Math.floor(Math.log(a) / Math.log(c))
return parseFloat((a / Math.pow(c, f)).toFixed(d)) + ' ' + e[f]
}