From 83ddc1bbc810a9f0d20dbf381cca5404cb7eb4c5 Mon Sep 17 00:00:00 2001 From: enes Date: Thu, 8 Aug 2024 17:30:49 +0200 Subject: [PATCH] feat: add dropzone and multiple files support --- package-lock.json | 45 ++++++++++++++++ package.json | 3 +- src/pages/create/index.tsx | 12 ++--- src/pages/home/index.tsx | 104 ++++++++++++++++++------------------- src/utils/nostr.ts | 1 + 5 files changed, 106 insertions(+), 59 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7c16799..23a0986 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "react-dnd": "16.0.1", "react-dnd-html5-backend": "16.0.1", "react-dom": "^18.2.0", + "react-dropzone": "^14.2.3", "react-redux": "9.1.0", "react-router-dom": "6.22.1", "react-toastify": "10.0.4", @@ -2680,6 +2681,15 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/attr-accept": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", + "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/axios": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", @@ -3857,6 +3867,24 @@ "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" }, + "node_modules/file-selector": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz", + "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/file-selector/node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "license": "0BSD" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -5716,6 +5744,23 @@ "react": "^18.2.0" } }, + "node_modules/react-dropzone": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz", + "integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==", + "license": "MIT", + "dependencies": { + "attr-accept": "^2.2.2", + "file-selector": "^0.6.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "react": ">= 16.8 || 18.0.0" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", diff --git a/package.json b/package.json index 976298b..a4ebe6f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 32", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 29", "lint:fix": "eslint . --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint:staged": "eslint --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "formatter:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"", @@ -47,6 +47,7 @@ "react-dnd": "16.0.1", "react-dnd-html5-backend": "16.0.1", "react-dom": "^18.2.0", + "react-dropzone": "^14.2.3", "react-redux": "9.1.0", "react-router-dom": "6.22.1", "react-toastify": "10.0.4", diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index e34b06d..9218b15 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -24,7 +24,7 @@ import JSZip from 'jszip' import { MuiFileInput } from 'mui-file-input' import { Event, kinds } from 'nostr-tools' import { useEffect, useRef, useState } from 'react' -import { DndProvider, DragSourceMonitor, useDrag, useDrop } from 'react-dnd' +import { DndProvider, useDrag, useDrop } from 'react-dnd' import { HTML5Backend } from 'react-dnd-html5-backend' import { useSelector } from 'react-redux' import { useLocation, useNavigate } from 'react-router-dom' @@ -68,7 +68,7 @@ import { Mark } from '../../types/mark.ts' export const CreatePage = () => { const navigate = useNavigate() const location = useLocation() - const { uploadedFile } = location.state || {} + const { uploadedFiles } = location.state || {} const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') @@ -134,10 +134,10 @@ export const CreatePage = () => { }) useEffect(() => { - if (uploadedFile) { - setSelectedFiles([uploadedFile]) + if (uploadedFiles) { + setSelectedFiles([...uploadedFiles]) } - }, [uploadedFile]) + }, [uploadedFiles]) useEffect(() => { if (usersPubkey) { @@ -979,7 +979,7 @@ const SignerRow = ({ item: () => { return { id: user.pubkey, index } }, - collect: (monitor: DragSourceMonitor) => ({ + collect: (monitor) => ({ isDragging: monitor.isDragging() }) }) diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index b6a801b..3689b3e 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -1,6 +1,6 @@ import { Button, TextField } from '@mui/material' import JSZip from 'jszip' -import { useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import { toast } from 'react-toastify' import { useAppSelector } from '../../hooks' @@ -10,7 +10,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faSearch } from '@fortawesome/free-solid-svg-icons' import { Select } from '../../components/Select' import { DisplaySigit } from '../../components/DisplaySigit' - +import { useDropzone } from 'react-dropzone' import { Container } from '../../components/Container' import styles from './style.module.scss' import { @@ -24,8 +24,8 @@ const FILTERS = [ 'Show all', // 'Drafts', 'In-progress', - 'Completed', - 'Archived' + 'Completed' + // 'Archived' ] as const type Filter = (typeof FILTERS)[number] @@ -40,7 +40,6 @@ type Sort = (typeof SORT_BY)[number]['value'] export const HomePage = () => { const navigate = useNavigate() - const fileInputRef = useRef(null) const [sigits, setSigits] = useState<{ [key: string]: Meta }>({}) const [parsedSigits, setParsedSigits] = useState<{ @@ -74,51 +73,52 @@ export const HomePage = () => { } }, [usersAppData]) - const handleUploadClick = () => { - if (fileInputRef.current) { - fileInputRef.current.click() - } - } + const onDrop = useCallback( + async (acceptedFiles: File[]) => { + // When uploading single file check if it's .sigit.zip + if (acceptedFiles.length === 1) { + const file = acceptedFiles[0] - const handleFileChange = async ( - event: React.ChangeEvent - ) => { - const file = event.target.files?.[0] - if (file) { - // Check if the file extension is .sigit.zip - const fileName = file.name - const fileExtension = fileName.slice(-10) // ".sigit.zip" has 10 characters - if (fileExtension === '.sigit.zip') { - const zip = await JSZip.loadAsync(file).catch((err) => { - console.log('err in loading zip file :>> ', err) - toast.error(err.message || 'An error occurred in loading zip file.') - return null - }) - - if (!zip) return - - // navigate to sign page if zip contains keys.json - if ('keys.json' in zip.files) { - return navigate(appPrivateRoutes.sign, { - state: { uploadedZip: file } + // Check if the file extension is .sigit.zip + const fileName = file.name + const fileExtension = fileName.slice(-10) // ".sigit.zip" has 10 characters + if (fileExtension === '.sigit.zip') { + const zip = await JSZip.loadAsync(file).catch((err) => { + console.log('err in loading zip file :>> ', err) + toast.error(err.message || 'An error occurred in loading zip file.') + return null }) - } - // navigate to verify page if zip contains meta.json - if ('meta.json' in zip.files) { - return navigate(appPublicRoutes.verify, { - state: { uploadedZip: file } - }) - } + if (!zip) return - toast.error('Invalid zip file') - return + // navigate to sign page if zip contains keys.json + if ('keys.json' in zip.files) { + return navigate(appPrivateRoutes.sign, { + state: { uploadedZip: file } + }) + } + + // navigate to verify page if zip contains meta.json + if ('meta.json' in zip.files) { + return navigate(appPublicRoutes.verify, { + state: { uploadedZip: file } + }) + } + + toast.error('Invalid SiGit zip file') + return + } } // navigate to create page - navigate(appPrivateRoutes.create, { state: { uploadedFile: file } }) - } - } + navigate(appPrivateRoutes.create, { + state: { uploadedFiles: acceptedFiles } + }) + }, + [navigate] + ) + + const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop }) const [search, setSearch] = useState('') const [filter, setFilter] = useState('Show all') @@ -202,15 +202,15 @@ export const HomePage = () => { -
- -
Click or drag files to upload!
+
+
+ + {isDragActive ? ( +

Drop the files here ...

+ ) : ( +

Click or drag files to upload!

+ )} +
{Object.keys(parsedSigits) diff --git a/src/utils/nostr.ts b/src/utils/nostr.ts index e27601b..bec854f 100644 --- a/src/utils/nostr.ts +++ b/src/utils/nostr.ts @@ -120,6 +120,7 @@ export const queryNip05 = async ( if (!match) throw new Error('Invalid nip05') // Destructure the match result, assigning default value '_' to name if not provided + // First variable from the match destructuring is ignored const [, name = '_', domain] = match // Construct the URL to query the NIP-05 data