From a2a6720fd8d1bed092022f2d05c332f1057b699c Mon Sep 17 00:00:00 2001 From: Matthias Kretschmann Date: Mon, 9 Sep 2019 13:53:06 +0200 Subject: [PATCH] dropzone component --- client/package-lock.json | 36 ++++++++++- client/package.json | 1 + .../components/molecules/Dropzone.module.scss | 36 +++++++++++ .../components/molecules/Dropzone.test.tsx | 52 ++++++++++++++++ client/src/components/molecules/Dropzone.tsx | 34 +++++++++++ client/src/hooks/use-ipfs-api.tsx | 17 ++++-- .../index.module.scss} | 2 +- .../{Ipfs.test.tsx => Ipfs/index.test.tsx} | 6 +- .../Files/{Ipfs.tsx => Ipfs/index.tsx} | 59 ++++++------------- .../routes/Publish/Files/Ipfs/utils.test.tsx | 9 +++ .../src/routes/Publish/Files/Ipfs/utils.tsx | 24 ++++++++ 11 files changed, 223 insertions(+), 53 deletions(-) create mode 100644 client/src/components/molecules/Dropzone.module.scss create mode 100644 client/src/components/molecules/Dropzone.test.tsx create mode 100644 client/src/components/molecules/Dropzone.tsx rename client/src/routes/Publish/Files/{Ipfs.module.scss => Ipfs/index.module.scss} (96%) rename client/src/routes/Publish/Files/{Ipfs.test.tsx => Ipfs/index.test.tsx} (72%) rename client/src/routes/Publish/Files/{Ipfs.tsx => Ipfs/index.tsx} (59%) create mode 100644 client/src/routes/Publish/Files/Ipfs/utils.test.tsx create mode 100644 client/src/routes/Publish/Files/Ipfs/utils.tsx diff --git a/client/package-lock.json b/client/package-lock.json index 208eda2..51da266 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -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", diff --git a/client/package.json b/client/package.json index 6c94a34..d3d0aaf 100644 --- a/client/package.json +++ b/client/package.json @@ -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", diff --git a/client/src/components/molecules/Dropzone.module.scss b/client/src/components/molecules/Dropzone.module.scss new file mode 100644 index 0000000..8f9b051 --- /dev/null +++ b/client/src/components/molecules/Dropzone.module.scss @@ -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; +} diff --git a/client/src/components/molecules/Dropzone.test.tsx b/client/src/components/molecules/Dropzone.test.tsx new file mode 100644 index 0000000..0bb858b --- /dev/null +++ b/client/src/components/molecules/Dropzone.test.tsx @@ -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 = + 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() +}) diff --git a/client/src/components/molecules/Dropzone.tsx b/client/src/components/molecules/Dropzone.tsx new file mode 100644 index 0000000..08974b6 --- /dev/null +++ b/client/src/components/molecules/Dropzone.tsx @@ -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 ( +
+ +

{`Drag 'n' drop some files here, or click to select files`}

+
+ ) +} diff --git a/client/src/hooks/use-ipfs-api.tsx b/client/src/hooks/use-ipfs-api.tsx index 2b2dd16..52f8ad9 100644 --- a/client/src/hooks/use-ipfs-api.tsx +++ b/client/src/hooks/use-ipfs-api.tsx @@ -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 } } diff --git a/client/src/routes/Publish/Files/Ipfs.module.scss b/client/src/routes/Publish/Files/Ipfs/index.module.scss similarity index 96% rename from client/src/routes/Publish/Files/Ipfs.module.scss rename to client/src/routes/Publish/Files/Ipfs/index.module.scss index d71a6b3..7412d24 100644 --- a/client/src/routes/Publish/Files/Ipfs.module.scss +++ b/client/src/routes/Publish/Files/Ipfs/index.module.scss @@ -1,4 +1,4 @@ -@import '../../../styles/variables'; +@import '../../../../styles/variables'; .ipfsForm { margin-top: $spacer / 2; diff --git a/client/src/routes/Publish/Files/Ipfs.test.tsx b/client/src/routes/Publish/Files/Ipfs/index.test.tsx similarity index 72% rename from client/src/routes/Publish/Files/Ipfs.test.tsx rename to client/src/routes/Publish/Files/Ipfs/index.test.tsx index db34471..25ab453 100644 --- a/client/src/routes/Publish/Files/Ipfs.test.tsx +++ b/client/src/routes/Publish/Files/Ipfs/index.test.tsx @@ -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 = + it('renders without crashing', async () => { - const { container, findByText } = render() + const { container, findByText } = render(ui) expect(container.firstChild).toBeInTheDocument() // wait for IPFS node diff --git a/client/src/routes/Publish/Files/Ipfs.tsx b/client/src/routes/Publish/Files/Ipfs/index.tsx similarity index 59% rename from client/src/routes/Publish/Files/Ipfs.tsx rename to client/src/routes/Publish/Files/Ipfs/index.tsx index cbe05d7..ab3cd01 100644 --- a/client/src/routes/Publish/Files/Ipfs.tsx +++ b/client/src/routes/Publish/Files/Ipfs/index.tsx @@ -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 ? ( ) : ( - handleCaptureFile(e.target.files)} - disabled={!isIpfsReady} - /> + )} {ipfsMessage !== '' && (
diff --git a/client/src/routes/Publish/Files/Ipfs/utils.test.tsx b/client/src/routes/Publish/Files/Ipfs/utils.test.tsx new file mode 100644 index 0000000..cc43eb2 --- /dev/null +++ b/client/src/routes/Publish/Files/Ipfs/utils.test.tsx @@ -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') + }) +}) diff --git a/client/src/routes/Publish/Files/Ipfs/utils.tsx b/client/src/routes/Publish/Files/Ipfs/utils.tsx new file mode 100644 index 0000000..c64937e --- /dev/null +++ b/client/src/routes/Publish/Files/Ipfs/utils.tsx @@ -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] +}