From dbf391defffa3b18082356209066188841c0e40e Mon Sep 17 00:00:00 2001 From: MTG2000 Date: Thu, 25 Aug 2022 15:57:59 +0300 Subject: [PATCH 01/20] feat: preview files comps, d&d component, uploading state tracking --- package-lock.json | 266 +++++++++++++++++- package.json | 4 + .../FileUploadInput.stories.tsx | 16 ++ .../FileUploadInput/FileUploadInput.tsx | 142 ++++++++++ .../Inputs/FileUploadInput/ImagePreviews.tsx | 92 ++++++ .../FileUploadInput/fetch-upload-img-url.tsx | 25 ++ .../Inputs/FileUploadInput/styles.module.scss | 25 ++ src/utils/storybook/decorators.tsx | 7 + 8 files changed, 575 insertions(+), 2 deletions(-) create mode 100644 src/Components/Inputs/FileUploadInput/FileUploadInput.stories.tsx create mode 100644 src/Components/Inputs/FileUploadInput/FileUploadInput.tsx create mode 100644 src/Components/Inputs/FileUploadInput/ImagePreviews.tsx create mode 100644 src/Components/Inputs/FileUploadInput/fetch-upload-img-url.tsx create mode 100644 src/Components/Inputs/FileUploadInput/styles.module.scss diff --git a/package-lock.json b/package-lock.json index b289b87..dac3b73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,10 @@ "@reduxjs/toolkit": "^1.8.1", "@remirror/pm": "^1.0.16", "@remirror/react": "^1.0.34", + "@rpldy/upload-button": "^1.0.1", + "@rpldy/upload-drop-zone": "^1.0.1", + "@rpldy/upload-preview": "^1.0.1", + "@rpldy/uploady": "^1.0.1", "@shopify/react-web-worker": "^5.0.1", "@szhsin/react-menu": "^3.0.2", "@testing-library/jest-dom": "^5.16.4", @@ -6937,6 +6941,156 @@ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==" }, + "node_modules/@rpldy/life-events": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rpldy/life-events/-/life-events-1.0.1.tgz", + "integrity": "sha512-z9b8Yi1jq4/Um0BQJVsKwwBps8jB+X6UMJXU3dG7Q4rHe7rIiAQw4fCjN5W3L4b9JF9jTs9yX+X7ouWvBLkPVw==", + "dependencies": { + "@rpldy/shared": "^1.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-uploady" + } + }, + "node_modules/@rpldy/sender": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rpldy/sender/-/sender-1.0.1.tgz", + "integrity": "sha512-5lGB2uPP22xESYhXdqzKKqZTtS03e0Gi9xx+1mu3XLEpUH7uu55dUMa8CT1cOzLM94JWwaS3fEQG+yB3n9Q7HQ==", + "dependencies": { + "@rpldy/shared": "^1.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-uploady" + } + }, + "node_modules/@rpldy/shared": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rpldy/shared/-/shared-1.0.1.tgz", + "integrity": "sha512-22R1ZI+J4vvD6JhHlevxYwn6PxSZ2eXmP1mHxkW/7MHgTWgcKD2xBhl2khfjub64rqnhhiU1KDumhAWEO3GoAw==", + "dependencies": { + "invariant": "^2.2.4", + "just-throttle": "^1.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-uploady" + } + }, + "node_modules/@rpldy/shared-ui": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rpldy/shared-ui/-/shared-ui-1.0.1.tgz", + "integrity": "sha512-pEKKifp4srk8vWyV7TAmpMRz5Dx66YcjOggP40ZZ7TV+CQfjY+b8TY1zx48ptakHqdpbG6Qzz0+OaA+VrOd3mA==", + "dependencies": { + "@rpldy/shared": "^1.0.1", + "@rpldy/uploader": "^1.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-uploady" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@rpldy/simple-state": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rpldy/simple-state/-/simple-state-1.0.1.tgz", + "integrity": "sha512-ku/WBl2RFCqMORGEL9/qklMTVN92mqaLbQAY7JtS+IhpG3hfIzwMEXh1eVAs8bpSU2nrsp2SeR1ykyxU8aFWiw==", + "dependencies": { + "@rpldy/shared": "^1.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-uploady" + } + }, + "node_modules/@rpldy/upload-button": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rpldy/upload-button/-/upload-button-1.0.1.tgz", + "integrity": "sha512-HuJuWlI9xljD7rQAmL5kEf40sOLZs8KF4VQCaj0y8+ErnEblmvjh/pSuRp+QgXVPT5NYrCq2YXSqOK9UYTBdsA==", + "dependencies": { + "@rpldy/shared-ui": "^1.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-uploady" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@rpldy/upload-drop-zone": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rpldy/upload-drop-zone/-/upload-drop-zone-1.0.1.tgz", + "integrity": "sha512-B6fhOuIrzCQhHKal+hoGkiubEVxjzFPja9RoYd5dOtaZ7aMwmTX5dtFgVyaL1hjit7H0npHkD6v27b4K6snCXw==", + "dependencies": { + "@rpldy/shared-ui": "^1.0.1", + "html-dir-content": "^0.3.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-uploady" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@rpldy/upload-preview": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rpldy/upload-preview/-/upload-preview-1.0.1.tgz", + "integrity": "sha512-Cq4+9fdgV2VRG4MsD9gnQ6AdRC4hMgPM4o88Iq7ifS5cDKNZa3hPVauSN39VpcoOZca1rDrdVslp9eStqmDTBg==", + "dependencies": { + "@rpldy/shared": "^1.0.1", + "@rpldy/shared-ui": "^1.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-uploady" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@rpldy/uploader": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rpldy/uploader/-/uploader-1.0.1.tgz", + "integrity": "sha512-QuFvKu/xdCtiQU8Szx/pH/3MIJvnTMtDlmN5P62GuHrgUJw9SypZCCv4hCLuq1LuHs7ZENplToPjvj3pscYFdA==", + "dependencies": { + "@rpldy/life-events": "^1.0.1", + "@rpldy/sender": "^1.0.1", + "@rpldy/shared": "^1.0.1", + "@rpldy/simple-state": "^1.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-uploady" + } + }, + "node_modules/@rpldy/uploady": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rpldy/uploady/-/uploady-1.0.1.tgz", + "integrity": "sha512-dwyNdYVy/wtioFfMUDsuAuHKKMi2/zMzKaiuOFGgPpx+3a9cVnbORNW/RcAMO6fVYLQgS4VxnqNSdnjS2m6buw==", + "dependencies": { + "@rpldy/life-events": "^1.0.1", + "@rpldy/shared": "^1.0.1", + "@rpldy/shared-ui": "^1.0.1", + "@rpldy/uploader": "^1.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-uploady" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/@rushstack/eslint-patch": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.1.3.tgz", @@ -27165,6 +27319,11 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/html-dir-content": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/html-dir-content/-/html-dir-content-0.3.2.tgz", + "integrity": "sha512-a1EJZbvBGmmFwk9VxFhEgaHkyXUXKTkw0jr0FCvXKCqgzO1H0wbFQbbzRA6FhR3twxAyjqVc80bzGHEmKrYsSw==" + }, "node_modules/html-encoding-sniffer": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", @@ -27779,7 +27938,6 @@ "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, "dependencies": { "loose-envify": "^1.0.0" } @@ -30938,6 +31096,11 @@ "node": ">=8" } }, + "node_modules/just-throttle": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/just-throttle/-/just-throttle-1.1.0.tgz", + "integrity": "sha512-iePC/13XYX1Tyn9C6jY+DG3UEejkDvrKsw5xxgGhtGUwYWmoJm4CoKexscBKELOu3FTyCDzjr21ZJ67AXnz+bg==" + }, "node_modules/jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -75915,6 +76078,96 @@ } } }, + "@rpldy/life-events": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rpldy/life-events/-/life-events-1.0.1.tgz", + "integrity": "sha512-z9b8Yi1jq4/Um0BQJVsKwwBps8jB+X6UMJXU3dG7Q4rHe7rIiAQw4fCjN5W3L4b9JF9jTs9yX+X7ouWvBLkPVw==", + "requires": { + "@rpldy/shared": "^1.0.1" + } + }, + "@rpldy/sender": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rpldy/sender/-/sender-1.0.1.tgz", + "integrity": "sha512-5lGB2uPP22xESYhXdqzKKqZTtS03e0Gi9xx+1mu3XLEpUH7uu55dUMa8CT1cOzLM94JWwaS3fEQG+yB3n9Q7HQ==", + "requires": { + "@rpldy/shared": "^1.0.1" + } + }, + "@rpldy/shared": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rpldy/shared/-/shared-1.0.1.tgz", + "integrity": "sha512-22R1ZI+J4vvD6JhHlevxYwn6PxSZ2eXmP1mHxkW/7MHgTWgcKD2xBhl2khfjub64rqnhhiU1KDumhAWEO3GoAw==", + "requires": { + "invariant": "^2.2.4", + "just-throttle": "^1.1.0" + } + }, + "@rpldy/shared-ui": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rpldy/shared-ui/-/shared-ui-1.0.1.tgz", + "integrity": "sha512-pEKKifp4srk8vWyV7TAmpMRz5Dx66YcjOggP40ZZ7TV+CQfjY+b8TY1zx48ptakHqdpbG6Qzz0+OaA+VrOd3mA==", + "requires": { + "@rpldy/shared": "^1.0.1", + "@rpldy/uploader": "^1.0.1" + } + }, + "@rpldy/simple-state": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rpldy/simple-state/-/simple-state-1.0.1.tgz", + "integrity": "sha512-ku/WBl2RFCqMORGEL9/qklMTVN92mqaLbQAY7JtS+IhpG3hfIzwMEXh1eVAs8bpSU2nrsp2SeR1ykyxU8aFWiw==", + "requires": { + "@rpldy/shared": "^1.0.1" + } + }, + "@rpldy/upload-button": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rpldy/upload-button/-/upload-button-1.0.1.tgz", + "integrity": "sha512-HuJuWlI9xljD7rQAmL5kEf40sOLZs8KF4VQCaj0y8+ErnEblmvjh/pSuRp+QgXVPT5NYrCq2YXSqOK9UYTBdsA==", + "requires": { + "@rpldy/shared-ui": "^1.0.1" + } + }, + "@rpldy/upload-drop-zone": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rpldy/upload-drop-zone/-/upload-drop-zone-1.0.1.tgz", + "integrity": "sha512-B6fhOuIrzCQhHKal+hoGkiubEVxjzFPja9RoYd5dOtaZ7aMwmTX5dtFgVyaL1hjit7H0npHkD6v27b4K6snCXw==", + "requires": { + "@rpldy/shared-ui": "^1.0.1", + "html-dir-content": "^0.3.2" + } + }, + "@rpldy/upload-preview": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rpldy/upload-preview/-/upload-preview-1.0.1.tgz", + "integrity": "sha512-Cq4+9fdgV2VRG4MsD9gnQ6AdRC4hMgPM4o88Iq7ifS5cDKNZa3hPVauSN39VpcoOZca1rDrdVslp9eStqmDTBg==", + "requires": { + "@rpldy/shared": "^1.0.1", + "@rpldy/shared-ui": "^1.0.1" + } + }, + "@rpldy/uploader": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rpldy/uploader/-/uploader-1.0.1.tgz", + "integrity": "sha512-QuFvKu/xdCtiQU8Szx/pH/3MIJvnTMtDlmN5P62GuHrgUJw9SypZCCv4hCLuq1LuHs7ZENplToPjvj3pscYFdA==", + "requires": { + "@rpldy/life-events": "^1.0.1", + "@rpldy/sender": "^1.0.1", + "@rpldy/shared": "^1.0.1", + "@rpldy/simple-state": "^1.0.1" + } + }, + "@rpldy/uploady": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rpldy/uploady/-/uploady-1.0.1.tgz", + "integrity": "sha512-dwyNdYVy/wtioFfMUDsuAuHKKMi2/zMzKaiuOFGgPpx+3a9cVnbORNW/RcAMO6fVYLQgS4VxnqNSdnjS2m6buw==", + "requires": { + "@rpldy/life-events": "^1.0.1", + "@rpldy/shared": "^1.0.1", + "@rpldy/shared-ui": "^1.0.1", + "@rpldy/uploader": "^1.0.1" + } + }, "@rushstack/eslint-patch": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.1.3.tgz", @@ -91882,6 +92135,11 @@ } } }, + "html-dir-content": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/html-dir-content/-/html-dir-content-0.3.2.tgz", + "integrity": "sha512-a1EJZbvBGmmFwk9VxFhEgaHkyXUXKTkw0jr0FCvXKCqgzO1H0wbFQbbzRA6FhR3twxAyjqVc80bzGHEmKrYsSw==" + }, "html-encoding-sniffer": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", @@ -92331,7 +92589,6 @@ "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, "requires": { "loose-envify": "^1.0.0" } @@ -94625,6 +94882,11 @@ "integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==", "dev": true }, + "just-throttle": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/just-throttle/-/just-throttle-1.1.0.tgz", + "integrity": "sha512-iePC/13XYX1Tyn9C6jY+DG3UEejkDvrKsw5xxgGhtGUwYWmoJm4CoKexscBKELOu3FTyCDzjr21ZJ67AXnz+bg==" + }, "jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", diff --git a/package.json b/package.json index 5bb2074..3292acf 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,10 @@ "@reduxjs/toolkit": "^1.8.1", "@remirror/pm": "^1.0.16", "@remirror/react": "^1.0.34", + "@rpldy/upload-button": "^1.0.1", + "@rpldy/upload-drop-zone": "^1.0.1", + "@rpldy/upload-preview": "^1.0.1", + "@rpldy/uploady": "^1.0.1", "@shopify/react-web-worker": "^5.0.1", "@szhsin/react-menu": "^3.0.2", "@testing-library/jest-dom": "^5.16.4", diff --git a/src/Components/Inputs/FileUploadInput/FileUploadInput.stories.tsx b/src/Components/Inputs/FileUploadInput/FileUploadInput.stories.tsx new file mode 100644 index 0000000..ba0cf5f --- /dev/null +++ b/src/Components/Inputs/FileUploadInput/FileUploadInput.stories.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import FileUploadInput from './FileUploadInput'; + +export default { + title: 'Shared/Inputs/File Upload Input', + component: FileUploadInput, + +} as ComponentMeta; + +const Template: ComponentStory = (args) => + + +export const DefaultButton = Template.bind({}); +DefaultButton.args = { +} \ No newline at end of file diff --git a/src/Components/Inputs/FileUploadInput/FileUploadInput.tsx b/src/Components/Inputs/FileUploadInput/FileUploadInput.tsx new file mode 100644 index 0000000..be68418 --- /dev/null +++ b/src/Components/Inputs/FileUploadInput/FileUploadInput.tsx @@ -0,0 +1,142 @@ +import Uploady, { useUploady, useRequestPreSend, UPLOADER_EVENTS } from "@rpldy/uploady"; +import { asUploadButton } from "@rpldy/upload-button"; +import Button from "src/Components/Button/Button"; +import { fetchUploadUrl } from "./fetch-upload-img-url"; +import ImagePreviews from "./ImagePreviews"; +import { FaImage } from "react-icons/fa"; +import UploadDropZone from "@rpldy/upload-drop-zone"; +import { forwardRef, useCallback } from "react"; +import styles from './styles.module.scss' +import { MdFileUpload } from "react-icons/md"; +import { AiOutlineCloudUpload } from "react-icons/ai"; +import { motion } from "framer-motion"; + +interface Props { + url: string; +} + +const UploadBtn = asUploadButton((props: any) => { + + useRequestPreSend(async (data) => { + + const filename = data.items?.[0].file.name ?? '' + + const url = await fetchUploadUrl({ filename }); + return { + options: { + destination: { + url + } + } + } + }) + + // const handleClick = async () => { + // // Make a request to get the url + // try { + // var bodyFormData = new FormData(); + // bodyFormData.append('requireSignedURLs', "false"); + // const res = await axios({ + // url: 'https://cors-anywhere.herokuapp.com/https://api.cloudflare.com/client/v4/accounts/783da4f06e5fdb9012c0632959a6f5b3/images/v2/direct_upload', + // method: 'POST', + // data: bodyFormData, + // headers: { + // "Authorization": "Bearer Xx2-CdsTliYkq6Ayz-1GX4CZubdQVxMwOSDbajP0", + // } + // }) + // uploady.upload(res.data.result.uploadUrl, { + // destination: res.data.result.uploadUrl + // }) + // } catch (error) { + // console.log(error); + + // } + + + // // make the request with the files + // // uploady.upload() + // } + + return +}); + + +const DropZone = forwardRef((props, ref) => { + const { onClick, ...buttonProps } = props; + + + useRequestPreSend(async (data) => { + + const filename = data.items?.[0].file.name ?? '' + + const url = await fetchUploadUrl({ filename }); + return { + options: { + destination: { + url + } + } + } + }) + + const onZoneClick = useCallback( + (e: any) => { + if (onClick) { + onClick(e); + } + }, + [onClick] + ); + + return +
+ Drop your files here or +
+ + + Drop it to upload + +
+}) + +const DropZoneButton = asUploadButton(DropZone); + + +export default function FileUploadInput(props: Props) { + return ( + { + const { id, filename, variants } = item?.uploadResponse?.data?.result ?? {} + if (id) { + console.log(id, filename, variants); + } + } + }} + > + + {/* */} + + + ) +} diff --git a/src/Components/Inputs/FileUploadInput/ImagePreviews.tsx b/src/Components/Inputs/FileUploadInput/ImagePreviews.tsx new file mode 100644 index 0000000..e28b375 --- /dev/null +++ b/src/Components/Inputs/FileUploadInput/ImagePreviews.tsx @@ -0,0 +1,92 @@ +import UploadPreview, { PreviewComponentProps, PreviewMethods } from '@rpldy/upload-preview' +import { useAbortItem, useItemAbortListener, useItemCancelListener, useItemErrorListener, useItemProgressListener } from '@rpldy/uploady'; +import React, { useState } from 'react' +import { RotatingLines } from 'react-loader-spinner'; + +export default function ImagePreviews() { + return ( +
+ +
+ ) +} + +function CustomImagePreview({ id, url }: PreviewComponentProps) { + + const [progress, setProgress] = useState(0); + const [itemState, setItemState] = useState(STATES.PROGRESS); + + + useItemProgressListener(item => { + if (item.completed > progress) { + setProgress(() => item.completed); + + if (item.completed === 100) { + setItemState(STATES.DONE) + } else { + setItemState(STATES.PROGRESS) + } + } + }, id); + + + + useItemAbortListener(item => { + setItemState(STATES.CANCELLED); + }, id); + + + useItemCancelListener(item => { + setItemState(STATES.CANCELLED); + }, id); + + useItemErrorListener(item => { + setItemState(STATES.ERROR); + }, id); + + return
+ +
+
+ {itemState === STATES.PROGRESS && +
+ +
} + {itemState === STATES.ERROR && +
+ Failed... +
} + {itemState === STATES.CANCELLED && +
+ Cancelled +
} +
; +}; + +const STATES = { + PROGRESS: "PROGRESS", + DONE: "DONE", + CANCELLED: "CANCELLED", + ERROR: "ERROR" +}; + +const STATE_COLORS = { + [STATES.PROGRESS]: "#f4e4a4", + [STATES.DONE]: "#a5f7b3", + [STATES.CANCELLED]: "#f7cdcd", + [STATES.ERROR]: "#ee4c4c" +}; \ No newline at end of file diff --git a/src/Components/Inputs/FileUploadInput/fetch-upload-img-url.tsx b/src/Components/Inputs/FileUploadInput/fetch-upload-img-url.tsx new file mode 100644 index 0000000..081677d --- /dev/null +++ b/src/Components/Inputs/FileUploadInput/fetch-upload-img-url.tsx @@ -0,0 +1,25 @@ +import axios from "axios"; +import { NotificationsService } from "src/services"; + +export async function fetchUploadUrl(options?: Partial<{ filename: string }>) { + + const { filename } = options ?? {} + + try { + const bodyFormData = new FormData(); + bodyFormData.append('requireSignedURLs', "false"); + const res = await axios({ + url: 'https://cors-anywhere.herokuapp.com/https://api.cloudflare.com/client/v4/accounts/783da4f06e5fdb9012c0632959a6f5b3/images/v2/direct_upload', + method: 'POST', + data: bodyFormData, + headers: { + "Authorization": "Bearer Xx2-CdsTliYkq6Ayz-1GX4CZubdQVxMwOSDbajP0", + } + }) + return res.data.result.uploadURL as string; + } catch (error) { + console.log(error); + NotificationsService.error("A network error happened.") + return "couldnt fetch upload url"; + } +} \ No newline at end of file diff --git a/src/Components/Inputs/FileUploadInput/styles.module.scss b/src/Components/Inputs/FileUploadInput/styles.module.scss new file mode 100644 index 0000000..217ca39 --- /dev/null +++ b/src/Components/Inputs/FileUploadInput/styles.module.scss @@ -0,0 +1,25 @@ +.zone { + background-color: #f2f4f7; + border-color: #e4e7ec; + + .active_content { + display: none; + } + + .idle_content { + display: block; + } + + &.active { + background-color: #b3a0ff; + border-color: #9e88ff; + + .active_content { + display: block; + } + + .idle_content { + display: none; + } + } +} diff --git a/src/utils/storybook/decorators.tsx b/src/utils/storybook/decorators.tsx index 4170b69..87f8df8 100644 --- a/src/utils/storybook/decorators.tsx +++ b/src/utils/storybook/decorators.tsx @@ -18,6 +18,8 @@ import { ApolloProvider } from '@apollo/client'; import { apolloClient } from '../apollo'; import { FormProvider, useForm, UseFormProps } from 'react-hook-form'; import ModalsContainer from 'src/Components/Modals/ModalsContainer/ModalsContainer'; +import { ToastContainer } from 'react-toastify'; +import { NotificationsService } from 'src/services'; // Enable the Mocks Service Worker @@ -63,6 +65,11 @@ export const WrapperDecorator: DecoratorFn = (Story, options) => { effect='solid' delayShow={1000} /> + ); } From 97e9b53abc83a6a8bf1cdd08154b47d5dbf8fccf Mon Sep 17 00:00:00 2001 From: MTG2000 Date: Thu, 25 Aug 2022 18:57:36 +0300 Subject: [PATCH 02/20] remove comments --- src/Components/Inputs/FileUploadInput/fetch-upload-img-url.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Inputs/FileUploadInput/fetch-upload-img-url.tsx b/src/Components/Inputs/FileUploadInput/fetch-upload-img-url.tsx index 081677d..e2c9b19 100644 --- a/src/Components/Inputs/FileUploadInput/fetch-upload-img-url.tsx +++ b/src/Components/Inputs/FileUploadInput/fetch-upload-img-url.tsx @@ -13,7 +13,7 @@ export async function fetchUploadUrl(options?: Partial<{ filename: string }>) { method: 'POST', data: bodyFormData, headers: { - "Authorization": "Bearer Xx2-CdsTliYkq6Ayz-1GX4CZubdQVxMwOSDbajP0", + "Authorization": "Bearer XXX", } }) return res.data.result.uploadURL as string; From 11614568532c9817e2365c2bd1b332f68354d946 Mon Sep 17 00:00:00 2001 From: MTG2000 Date: Sat, 27 Aug 2022 12:07:17 +0300 Subject: [PATCH 03/20] feat: screenshots files input --- package-lock.json | 25 +++ package.json | 1 + .../Inputs/ScreenshotsInput/ImagePreviews.tsx | 97 +++++++++++ .../ScreenshotsInput/ScreenshotThumbnail.tsx | 52 ++++++ .../ScreenshotsInput.stories.tsx | 24 +++ .../ScreenshotsInput/ScreenshotsInput.tsx | 151 ++++++++++++++++++ .../ScreenshotsInput/styles.module.scss | 25 +++ src/utils/storybook/decorators.tsx | 7 +- 8 files changed, 381 insertions(+), 1 deletion(-) create mode 100644 src/Components/Inputs/ScreenshotsInput/ImagePreviews.tsx create mode 100644 src/Components/Inputs/ScreenshotsInput/ScreenshotThumbnail.tsx create mode 100644 src/Components/Inputs/ScreenshotsInput/ScreenshotsInput.stories.tsx create mode 100644 src/Components/Inputs/ScreenshotsInput/ScreenshotsInput.tsx create mode 100644 src/Components/Inputs/ScreenshotsInput/styles.module.scss diff --git a/package-lock.json b/package-lock.json index dac3b73..eb53b0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@reduxjs/toolkit": "^1.8.1", "@remirror/pm": "^1.0.16", "@remirror/react": "^1.0.34", + "@rpldy/mock-sender": "^1.0.1", "@rpldy/upload-button": "^1.0.1", "@rpldy/upload-drop-zone": "^1.0.1", "@rpldy/upload-preview": "^1.0.1", @@ -6953,6 +6954,20 @@ "url": "https://opencollective.com/react-uploady" } }, + "node_modules/@rpldy/mock-sender": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rpldy/mock-sender/-/mock-sender-1.0.1.tgz", + "integrity": "sha512-87UT/az8J2AD6NIYX/uVp6GUqM/M4vVpZsXod14GkNkAOOilGow6R1W/3PW7lxx8bjfM51Moraipq1KNwlIODA==", + "dependencies": { + "@rpldy/sender": "^1.0.1", + "@rpldy/shared": "^1.0.1", + "@rpldy/uploader": "^1.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-uploady" + } + }, "node_modules/@rpldy/sender": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@rpldy/sender/-/sender-1.0.1.tgz", @@ -76086,6 +76101,16 @@ "@rpldy/shared": "^1.0.1" } }, + "@rpldy/mock-sender": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rpldy/mock-sender/-/mock-sender-1.0.1.tgz", + "integrity": "sha512-87UT/az8J2AD6NIYX/uVp6GUqM/M4vVpZsXod14GkNkAOOilGow6R1W/3PW7lxx8bjfM51Moraipq1KNwlIODA==", + "requires": { + "@rpldy/sender": "^1.0.1", + "@rpldy/shared": "^1.0.1", + "@rpldy/uploader": "^1.0.1" + } + }, "@rpldy/sender": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@rpldy/sender/-/sender-1.0.1.tgz", diff --git a/package.json b/package.json index 3292acf..0dc6f1f 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@reduxjs/toolkit": "^1.8.1", "@remirror/pm": "^1.0.16", "@remirror/react": "^1.0.34", + "@rpldy/mock-sender": "^1.0.1", "@rpldy/upload-button": "^1.0.1", "@rpldy/upload-drop-zone": "^1.0.1", "@rpldy/upload-preview": "^1.0.1", diff --git a/src/Components/Inputs/ScreenshotsInput/ImagePreviews.tsx b/src/Components/Inputs/ScreenshotsInput/ImagePreviews.tsx new file mode 100644 index 0000000..473ce81 --- /dev/null +++ b/src/Components/Inputs/ScreenshotsInput/ImagePreviews.tsx @@ -0,0 +1,97 @@ +import UploadPreview, { PreviewComponentProps, PreviewMethods } from '@rpldy/upload-preview' +import { useAbortItem, useItemAbortListener, useItemCancelListener, useItemErrorListener, useItemProgressListener } from '@rpldy/uploady'; +import { useState } from 'react' +import ScreenShotsThumbnail from './ScreenshotThumbnail' + +export default function ImagePreviews() { + return ( + + ) +} + +function CustomImagePreview({ id, url }: PreviewComponentProps) { + + const [progress, setProgress] = useState(0); + const [itemState, setItemState] = useState(STATES.PROGRESS); + + const abortItem = useAbortItem(); + + + useItemProgressListener(item => { + if (item.completed > progress) { + setProgress(() => item.completed); + + if (item.completed === 100) { + setItemState(STATES.DONE) + } else { + setItemState(STATES.PROGRESS) + } + } + }, id); + + + + useItemAbortListener(item => { + setItemState(STATES.CANCELLED); + }, id); + + + useItemCancelListener(item => { + setItemState(STATES.CANCELLED); + }, id); + + useItemErrorListener(item => { + setItemState(STATES.ERROR); + }, id); + + if (itemState === STATES.DONE) + return null + + return { + abortItem(id) + }} + /> + + // return
+ // + //
+ //
+ // {itemState === STATES.PROGRESS && + //
+ // + //
} + // {itemState === STATES.ERROR && + //
+ // Failed... + //
} + // {itemState === STATES.CANCELLED && + //
+ // Cancelled + //
} + //
; +}; + +const STATES = { + PROGRESS: "PROGRESS", + DONE: "DONE", + CANCELLED: "CANCELLED", + ERROR: "ERROR" +}; diff --git a/src/Components/Inputs/ScreenshotsInput/ScreenshotThumbnail.tsx b/src/Components/Inputs/ScreenshotsInput/ScreenshotThumbnail.tsx new file mode 100644 index 0000000..0003c43 --- /dev/null +++ b/src/Components/Inputs/ScreenshotsInput/ScreenshotThumbnail.tsx @@ -0,0 +1,52 @@ +import React from 'react' +import { FaTimes } from 'react-icons/fa'; +import { RotatingLines } from 'react-loader-spinner'; + +interface Props { + url?: string, + isLoading?: boolean; + isError?: boolean; + onCancel?: () => void; + +} + +export default function ScreenshotThumbnail({ url, isLoading, isError, onCancel }: Props) { + + const isEmpty = !url; + + return ( +
+ {!isEmpty && } +
+
+ {isLoading && +
+ +
} + {isError && +
+ Failed... +
} + {!isEmpty && + + } +
+ ) +} diff --git a/src/Components/Inputs/ScreenshotsInput/ScreenshotsInput.stories.tsx b/src/Components/Inputs/ScreenshotsInput/ScreenshotsInput.stories.tsx new file mode 100644 index 0000000..ebd46c2 --- /dev/null +++ b/src/Components/Inputs/ScreenshotsInput/ScreenshotsInput.stories.tsx @@ -0,0 +1,24 @@ +import React from 'react' +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import ScreenshotsInput from './ScreenshotsInput'; +import { WrapForm } from 'src/utils/storybook/decorators'; + +export default { + title: 'Shared/Inputs/Screenshots Input', + component: ScreenshotsInput, + decorators: [ + WrapForm<{ screenshots: Array }>({ + logValues: true, + defaultValues: { + screenshots: [] + } + })] +} as ComponentMeta; + +const Template: ComponentStory = (args) => + + +export const DefaultButton = Template.bind({}); +DefaultButton.args = { + name: 'screenshots', +} \ No newline at end of file diff --git a/src/Components/Inputs/ScreenshotsInput/ScreenshotsInput.tsx b/src/Components/Inputs/ScreenshotsInput/ScreenshotsInput.tsx new file mode 100644 index 0000000..107ca24 --- /dev/null +++ b/src/Components/Inputs/ScreenshotsInput/ScreenshotsInput.tsx @@ -0,0 +1,151 @@ +import Uploady, { useRequestPreSend, UPLOADER_EVENTS } from "@rpldy/uploady"; +import { asUploadButton } from "@rpldy/upload-button"; +// import { fetchUploadUrl } from "./fetch-upload-img-url"; +import ImagePreviews from "./ImagePreviews"; +import UploadDropZone from "@rpldy/upload-drop-zone"; +import { forwardRef, useCallback, useState } from "react"; +import styles from './styles.module.scss' +import { AiOutlineCloudUpload } from "react-icons/ai"; +import { motion } from "framer-motion"; +import { getMockSenderEnhancer } from "@rpldy/mock-sender"; +import ScreenshotThumbnail from "./ScreenshotThumbnail"; +import { FiCamera } from "react-icons/fi"; +import { Control, Path, useController } from "react-hook-form"; + +interface Props { + control?: Control, + name?: Path | string +} + + +const mockSenderEnhancer = getMockSenderEnhancer({ + delay: 1500, +}); + +const MAX_UPLOAD_COUNT = 4 as const; + + + +export default function ScreenshotsInput(props: Props) { + + const { field: { value: uploadedFiles, onChange } } = useController({ + control: props.control, + name: props.name ?? 'screenshots' as any, + }) + + + const [uploadingCount, setUploadingCount] = useState(0) + + if (!Array.isArray(uploadedFiles)) + throw new Error("screenshots field should be an array"); + + + const canUploadMore = uploadingCount + uploadedFiles.length < MAX_UPLOAD_COUNT; + const placeholdersCount = (MAX_UPLOAD_COUNT - (uploadingCount + uploadedFiles.length + 1)); + + + return ( + { + setUploadingCount(v => v + batch.items.length) + }, + [UPLOADER_EVENTS.ITEM_FINALIZE]: () => setUploadingCount(v => v - 1), + [UPLOADER_EVENTS.ITEM_FINISH]: (item) => { + + // Just for mocking purposes + const dataUrl = URL.createObjectURL(item.file); + + const { id, filename, variants } = item?.uploadResponse?.data?.result ?? { + id: Math.random().toString(), + filename: item.file.name, + variants: [ + "", + dataUrl + ] + } + if (id) { + onChange([...uploadedFiles, { id, name: filename, url: variants[1] }]) + } + } + }} + > + +
+ {canUploadMore && } + {uploadedFiles.map(f => { + onChange(uploadedFiles.filter(file => file.id !== f.id)) + }} />)} + + {(placeholdersCount > 0) && + Array(placeholdersCount).fill(0).map((_, idx) => )} +
+
+ ) +} + +const DropZone = forwardRef((props, ref) => { + const { onClick, ...buttonProps } = props; + + + useRequestPreSend(async (data) => { + + const filename = data.items?.[0].file.name ?? '' + + // const url = await fetchUploadUrl({ filename }); + return { + options: { + destination: { + url: "URL" + } + } + } + }) + + const onZoneClick = useCallback( + (e: any) => { + if (onClick) { + onClick(e); + } + }, + [onClick] + ); + + return +
+

+
+ Browse images or
drop + them here +
+
+ + + Drop to upload
+
+
+}) + +const DropZoneButton = asUploadButton(DropZone); diff --git a/src/Components/Inputs/ScreenshotsInput/styles.module.scss b/src/Components/Inputs/ScreenshotsInput/styles.module.scss new file mode 100644 index 0000000..217ca39 --- /dev/null +++ b/src/Components/Inputs/ScreenshotsInput/styles.module.scss @@ -0,0 +1,25 @@ +.zone { + background-color: #f2f4f7; + border-color: #e4e7ec; + + .active_content { + display: none; + } + + .idle_content { + display: block; + } + + &.active { + background-color: #b3a0ff; + border-color: #9e88ff; + + .active_content { + display: block; + } + + .idle_content { + display: none; + } + } +} diff --git a/src/utils/storybook/decorators.tsx b/src/utils/storybook/decorators.tsx index 87f8df8..2c4f460 100644 --- a/src/utils/storybook/decorators.tsx +++ b/src/utils/storybook/decorators.tsx @@ -119,9 +119,14 @@ export const centerDecorator: DecoratorFn = (Story) => { } -export function WrapForm(options?: Partial>): DecoratorFn { +export function WrapForm(options?: Partial & { logValues: boolean }>): DecoratorFn { const Func: DecoratorFn = (Story) => { const methods = useForm(options); + + if (options?.logValues) { + console.log(methods.watch()) + } + return From 1ed976b211c8ae21ce257ad9258189bb25762151 Mon Sep 17 00:00:00 2001 From: MTG2000 Date: Sat, 27 Aug 2022 13:05:05 +0300 Subject: [PATCH 04/20] feat: stories & state management for the screenshots input --- .../Inputs/ScreenshotsInput/ImagePreviews.tsx | 2 +- .../ScreenshotsInput.stories.tsx | 74 +++++++++++++++++-- .../ScreenshotsInput/ScreenshotsInput.tsx | 23 +++--- src/redux/features/modals.slice.ts | 1 + src/utils/storybook/decorators.tsx | 25 ++++++- 5 files changed, 105 insertions(+), 20 deletions(-) diff --git a/src/Components/Inputs/ScreenshotsInput/ImagePreviews.tsx b/src/Components/Inputs/ScreenshotsInput/ImagePreviews.tsx index 473ce81..74d4d64 100644 --- a/src/Components/Inputs/ScreenshotsInput/ImagePreviews.tsx +++ b/src/Components/Inputs/ScreenshotsInput/ImagePreviews.tsx @@ -44,7 +44,7 @@ function CustomImagePreview({ id, url }: PreviewComponentProps) { setItemState(STATES.ERROR); }, id); - if (itemState === STATES.DONE) + if (itemState === STATES.DONE || itemState === STATES.CANCELLED) return null return }>({ + WrapFormController<{ screenshots: Array }>({ logValues: true, + name: "screenshots", defaultValues: { screenshots: [] } })] } as ComponentMeta; -const Template: ComponentStory = (args) => +const Template: ComponentStory = (args, context) => { + + return + +} -export const DefaultButton = Template.bind({}); -DefaultButton.args = { - name: 'screenshots', +export const Empty = Template.bind({}); +Empty.args = { +} + +export const WithValues = Template.bind({}); +WithValues.decorators = [ + WrapFormController<{ screenshots: Array }>({ + logValues: true, + name: "screenshots", + defaultValues: { + screenshots: [{ + id: '123', + name: 'tree', + url: "https://picsum.photos/id/1021/800/800.jpg" + }, + { + id: '555', + name: 'whatever', + url: "https://picsum.photos/id/600/800/800.jpg" + },] + } + }) as any +]; +WithValues.args = { +} + +export const Full = Template.bind({}); +Full.decorators = [ + WrapFormController<{ screenshots: Array }>({ + logValues: true, + name: "screenshots", + defaultValues: { + screenshots: [ + { + id: '123', + name: 'tree', + url: "https://picsum.photos/id/1021/800/800.jpg" + }, + { + id: '555', + name: 'whatever', + url: "https://picsum.photos/id/600/800/800.jpg" + }, + { + id: '562', + name: 'Moon', + url: "https://picsum.photos/id/32/800/800.jpg" + }, + { + id: '342', + name: 'Sun', + url: "https://picsum.photos/id/523/800/800.jpg" + }, + ] + } + }) as any +]; +Full.args = { } \ No newline at end of file diff --git a/src/Components/Inputs/ScreenshotsInput/ScreenshotsInput.tsx b/src/Components/Inputs/ScreenshotsInput/ScreenshotsInput.tsx index 107ca24..422f29b 100644 --- a/src/Components/Inputs/ScreenshotsInput/ScreenshotsInput.tsx +++ b/src/Components/Inputs/ScreenshotsInput/ScreenshotsInput.tsx @@ -12,10 +12,6 @@ import ScreenshotThumbnail from "./ScreenshotThumbnail"; import { FiCamera } from "react-icons/fi"; import { Control, Path, useController } from "react-hook-form"; -interface Props { - control?: Control, - name?: Path | string -} const mockSenderEnhancer = getMockSenderEnhancer({ @@ -24,14 +20,21 @@ const mockSenderEnhancer = getMockSenderEnhancer({ const MAX_UPLOAD_COUNT = 4 as const; +export interface ScreenshotType { + id: string, + name: string, + url: string; +} + +interface Props { + value: ScreenshotType[], + onChange: (new_value: ScreenshotType[]) => void +} -export default function ScreenshotsInput(props: Props) { +export default function ScreenshotsInput(props: Props) { - const { field: { value: uploadedFiles, onChange } } = useController({ - control: props.control, - name: props.name ?? 'screenshots' as any, - }) + const { value: uploadedFiles, onChange } = props; const [uploadingCount, setUploadingCount] = useState(0) @@ -69,7 +72,7 @@ export default function ScreenshotsInput(props: Props) { ] } if (id) { - onChange([...uploadedFiles, { id, name: filename, url: variants[1] }]) + onChange([...uploadedFiles, { id, name: filename, url: variants[1] }].slice(-MAX_UPLOAD_COUNT)) } } }} diff --git a/src/redux/features/modals.slice.ts b/src/redux/features/modals.slice.ts index f4e547e..f04a320 100644 --- a/src/redux/features/modals.slice.ts +++ b/src/redux/features/modals.slice.ts @@ -5,6 +5,7 @@ import VoteCard from "src/features/Projects/pages/ProjectPage/VoteCard/VoteCard" import { InsertImageModal } from 'src/Components/Inputs/TextEditor/InsertImageModal' import { InsertVideoModal } from 'src/Components/Inputs/TextEditor/InsertVideoModal' import { InsertLinkModal } from 'src/Components/Inputs/TextEditor/InsertLinkModal' + import { Claim_FundWithdrawCard, Claim_CopySignatureCard, Claim_GenerateSignatureCard, Claim_SubmittedCard } from "src/features/Projects/pages/ProjectPage/ClaimProject"; import { ModalCard } from "src/Components/Modals/ModalsContainer/ModalsContainer"; import { ConfirmModal } from "src/Components/Modals/ConfirmModal"; diff --git a/src/utils/storybook/decorators.tsx b/src/utils/storybook/decorators.tsx index 2c4f460..9a11a1d 100644 --- a/src/utils/storybook/decorators.tsx +++ b/src/utils/storybook/decorators.tsx @@ -16,7 +16,7 @@ import "src/styles/index.scss"; import 'react-loading-skeleton/dist/skeleton.css' import { ApolloProvider } from '@apollo/client'; import { apolloClient } from '../apollo'; -import { FormProvider, useForm, UseFormProps } from 'react-hook-form'; +import { Controller, FormProvider, useForm, UseFormProps } from 'react-hook-form'; import ModalsContainer from 'src/Components/Modals/ModalsContainer/ModalsContainer'; import { ToastContainer } from 'react-toastify'; import { NotificationsService } from 'src/services'; @@ -128,12 +128,33 @@ export function WrapForm(options?: Partial & { logValue } return - + } return Func } +export function WrapFormController(options: Partial & { logValues: boolean }> & { name: string }): DecoratorFn { + const Func: DecoratorFn = (Story) => { + + const methods = useForm(options); + + if (options?.logValues) { + console.log(methods.watch(options.name as any)) + } + + return + + } + /> + } + return Func +} + + export const WithModals: DecoratorFn = (Component) => <> From 1735f9a84f01daa723b8b56d7aa63d0ca705e43e Mon Sep 17 00:00:00 2001 From: MTG2000 Date: Sat, 27 Aug 2022 14:45:03 +0300 Subject: [PATCH 05/20] feat: Built the Inplace Image Uploading component and functionality --- .../FileUploadInput.stories.tsx | 2 +- .../FileUploadInput/FileUploadInput.tsx | 7 +- .../Inputs/FilesInput/FileInput.stories.tsx | 31 ---- .../ScreenshotsInput.stories.tsx | 2 +- .../ScreenshotsInput/ScreenshotsInput.tsx | 3 - .../SingleImageUploadInput/ImagePreviews.tsx | 97 ++++++++++++ .../ScreenshotThumbnail.tsx | 52 +++++++ .../SingleImageUploadInput.stories.tsx | 118 +++++++++++++++ .../SingleImageUploadInput.tsx | 139 ++++++++++++++++++ .../SingleImageUploadInput/styles.module.scss | 25 ++++ 10 files changed, 436 insertions(+), 40 deletions(-) delete mode 100644 src/Components/Inputs/FilesInput/FileInput.stories.tsx create mode 100644 src/Components/Inputs/SingleImageUploadInput/ImagePreviews.tsx create mode 100644 src/Components/Inputs/SingleImageUploadInput/ScreenshotThumbnail.tsx create mode 100644 src/Components/Inputs/SingleImageUploadInput/SingleImageUploadInput.stories.tsx create mode 100644 src/Components/Inputs/SingleImageUploadInput/SingleImageUploadInput.tsx create mode 100644 src/Components/Inputs/SingleImageUploadInput/styles.module.scss diff --git a/src/Components/Inputs/FileUploadInput/FileUploadInput.stories.tsx b/src/Components/Inputs/FileUploadInput/FileUploadInput.stories.tsx index ba0cf5f..2e5b4dd 100644 --- a/src/Components/Inputs/FileUploadInput/FileUploadInput.stories.tsx +++ b/src/Components/Inputs/FileUploadInput/FileUploadInput.stories.tsx @@ -3,7 +3,7 @@ import { ComponentStory, ComponentMeta } from '@storybook/react'; import FileUploadInput from './FileUploadInput'; export default { - title: 'Shared/Inputs/File Upload Input', + title: 'Shared/Inputs/Files Inputs/Basic', component: FileUploadInput, } as ComponentMeta; diff --git a/src/Components/Inputs/FileUploadInput/FileUploadInput.tsx b/src/Components/Inputs/FileUploadInput/FileUploadInput.tsx index be68418..241a317 100644 --- a/src/Components/Inputs/FileUploadInput/FileUploadInput.tsx +++ b/src/Components/Inputs/FileUploadInput/FileUploadInput.tsx @@ -1,4 +1,4 @@ -import Uploady, { useUploady, useRequestPreSend, UPLOADER_EVENTS } from "@rpldy/uploady"; +import Uploady, { useUploady, useRequestPreSend, UPLOADER_EVENTS, } from "@rpldy/uploady"; import { asUploadButton } from "@rpldy/upload-button"; import Button from "src/Components/Button/Button"; import { fetchUploadUrl } from "./fetch-upload-img-url"; @@ -66,7 +66,6 @@ const UploadBtn = asUploadButton((props: any) => { const DropZone = forwardRef((props, ref) => { const { onClick, ...buttonProps } = props; - useRequestPreSend(async (data) => { const filename = data.items?.[0].file.name ?? '' @@ -95,10 +94,10 @@ const DropZone = forwardRef((props, ref) => { ref={ref} onDragOverClassName={styles.active} extraProps={{ onClick: onZoneClick }} - className={`${styles.zone} border-2 w-full min-h-[200px] rounded-16 flex flex-col justify-center items-center text text-body3`} + className={`${styles.zone} border-2 w-full min-h-[200px] max-w-[600px] rounded-16 flex flex-col justify-center items-center text text-body3 border-dashed`} >
- Drop your files here or + Drop your IMAGES here or
; - -const Template: ComponentStory = (args) => - - -export const DefaultButton = Template.bind({}); -DefaultButton.args = { -} - -export const CustomizedButton = Template.bind({}); -CustomizedButton.args = { - multiple: true, - uploadBtn: -} - -const DropTemplate: ComponentStory = (args) =>
-export const DropZoneInput = DropTemplate.bind({}); -DropZoneInput.args = { - onChange: console.log, -} \ No newline at end of file diff --git a/src/Components/Inputs/ScreenshotsInput/ScreenshotsInput.stories.tsx b/src/Components/Inputs/ScreenshotsInput/ScreenshotsInput.stories.tsx index 63fda1d..1575707 100644 --- a/src/Components/Inputs/ScreenshotsInput/ScreenshotsInput.stories.tsx +++ b/src/Components/Inputs/ScreenshotsInput/ScreenshotsInput.stories.tsx @@ -4,7 +4,7 @@ import ScreenshotsInput, { ScreenshotType } from './ScreenshotsInput'; import { WrapForm, WrapFormController } from 'src/utils/storybook/decorators'; export default { - title: 'Shared/Inputs/Screenshots Input', + title: 'Shared/Inputs/Files Inputs/Screenshots', component: ScreenshotsInput, decorators: [ WrapFormController<{ screenshots: Array }>({ diff --git a/src/Components/Inputs/ScreenshotsInput/ScreenshotsInput.tsx b/src/Components/Inputs/ScreenshotsInput/ScreenshotsInput.tsx index 422f29b..b5bf6c0 100644 --- a/src/Components/Inputs/ScreenshotsInput/ScreenshotsInput.tsx +++ b/src/Components/Inputs/ScreenshotsInput/ScreenshotsInput.tsx @@ -39,9 +39,6 @@ export default function ScreenshotsInput(props: Props) { const [uploadingCount, setUploadingCount] = useState(0) - if (!Array.isArray(uploadedFiles)) - throw new Error("screenshots field should be an array"); - const canUploadMore = uploadingCount + uploadedFiles.length < MAX_UPLOAD_COUNT; const placeholdersCount = (MAX_UPLOAD_COUNT - (uploadingCount + uploadedFiles.length + 1)); diff --git a/src/Components/Inputs/SingleImageUploadInput/ImagePreviews.tsx b/src/Components/Inputs/SingleImageUploadInput/ImagePreviews.tsx new file mode 100644 index 0000000..74d4d64 --- /dev/null +++ b/src/Components/Inputs/SingleImageUploadInput/ImagePreviews.tsx @@ -0,0 +1,97 @@ +import UploadPreview, { PreviewComponentProps, PreviewMethods } from '@rpldy/upload-preview' +import { useAbortItem, useItemAbortListener, useItemCancelListener, useItemErrorListener, useItemProgressListener } from '@rpldy/uploady'; +import { useState } from 'react' +import ScreenShotsThumbnail from './ScreenshotThumbnail' + +export default function ImagePreviews() { + return ( + + ) +} + +function CustomImagePreview({ id, url }: PreviewComponentProps) { + + const [progress, setProgress] = useState(0); + const [itemState, setItemState] = useState(STATES.PROGRESS); + + const abortItem = useAbortItem(); + + + useItemProgressListener(item => { + if (item.completed > progress) { + setProgress(() => item.completed); + + if (item.completed === 100) { + setItemState(STATES.DONE) + } else { + setItemState(STATES.PROGRESS) + } + } + }, id); + + + + useItemAbortListener(item => { + setItemState(STATES.CANCELLED); + }, id); + + + useItemCancelListener(item => { + setItemState(STATES.CANCELLED); + }, id); + + useItemErrorListener(item => { + setItemState(STATES.ERROR); + }, id); + + if (itemState === STATES.DONE || itemState === STATES.CANCELLED) + return null + + return { + abortItem(id) + }} + /> + + // return
+ // + //
+ //
+ // {itemState === STATES.PROGRESS && + //
+ // + //
} + // {itemState === STATES.ERROR && + //
+ // Failed... + //
} + // {itemState === STATES.CANCELLED && + //
+ // Cancelled + //
} + //
; +}; + +const STATES = { + PROGRESS: "PROGRESS", + DONE: "DONE", + CANCELLED: "CANCELLED", + ERROR: "ERROR" +}; diff --git a/src/Components/Inputs/SingleImageUploadInput/ScreenshotThumbnail.tsx b/src/Components/Inputs/SingleImageUploadInput/ScreenshotThumbnail.tsx new file mode 100644 index 0000000..0003c43 --- /dev/null +++ b/src/Components/Inputs/SingleImageUploadInput/ScreenshotThumbnail.tsx @@ -0,0 +1,52 @@ +import React from 'react' +import { FaTimes } from 'react-icons/fa'; +import { RotatingLines } from 'react-loader-spinner'; + +interface Props { + url?: string, + isLoading?: boolean; + isError?: boolean; + onCancel?: () => void; + +} + +export default function ScreenshotThumbnail({ url, isLoading, isError, onCancel }: Props) { + + const isEmpty = !url; + + return ( +
+ {!isEmpty && } +
+
+ {isLoading && +
+ +
} + {isError && +
+ Failed... +
} + {!isEmpty && + + } +
+ ) +} diff --git a/src/Components/Inputs/SingleImageUploadInput/SingleImageUploadInput.stories.tsx b/src/Components/Inputs/SingleImageUploadInput/SingleImageUploadInput.stories.tsx new file mode 100644 index 0000000..e2d9061 --- /dev/null +++ b/src/Components/Inputs/SingleImageUploadInput/SingleImageUploadInput.stories.tsx @@ -0,0 +1,118 @@ +import React from 'react' +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import SingleImageUploadInput, { ImageType } from './SingleImageUploadInput'; +import { WrapFormController } from 'src/utils/storybook/decorators'; +import { RotatingLines } from 'react-loader-spinner'; +import { FiCamera, } from 'react-icons/fi'; +import { FaExchangeAlt, FaImage } from 'react-icons/fa'; + +export default { + title: 'Shared/Inputs/Files Inputs/Single Image Upload ', + component: SingleImageUploadInput, + decorators: [ + WrapFormController<{ avatar: ImageType | null }>({ + logValues: true, + name: "avatar", + defaultValues: { + avatar: null + } + })] +} as ComponentMeta; + +const Template: ComponentStory = (args, context) => { + + return + +} + + +export const Avatar = Template.bind({}); +Avatar.args = { + wrapperClass: "inline-block cursor-pointer ", + render: ({ img, isUploading }) =>
+ {img && } + {!img && + <> +

+
+ Add Image +
+ } + {isUploading && +
+ +
+ } +
+} + +export const Thumbnail = Template.bind({}); +Thumbnail.args = { + wrapperClass: "inline-block cursor-pointer ", + render: ({ img, isUploading }) =>
+ {img && } + {!img && + <> +

+
+ Add Image +
+ } + {isUploading && +
+ +
+ } +
+} + +export const Cover = Template.bind({}); +Cover.args = { + wrapperClass: "block cursor-pointer ", + render: ({ img, isUploading }) =>
+ {img && <> + + {!isUploading && + } + } + {!img && + <> +

+
+ Drop a COVER IMAGE here or
Click to browse +
+ } + {isUploading && +
+ +
+ } +
+} + diff --git a/src/Components/Inputs/SingleImageUploadInput/SingleImageUploadInput.tsx b/src/Components/Inputs/SingleImageUploadInput/SingleImageUploadInput.tsx new file mode 100644 index 0000000..f95cd31 --- /dev/null +++ b/src/Components/Inputs/SingleImageUploadInput/SingleImageUploadInput.tsx @@ -0,0 +1,139 @@ +import Uploady, { useRequestPreSend, UPLOADER_EVENTS, useAbortAll } from "@rpldy/uploady"; +import { asUploadButton } from "@rpldy/upload-button"; +// import { fetchUploadUrl } from "./fetch-upload-img-url"; +import UploadDropZone from "@rpldy/upload-drop-zone"; +import { forwardRef, ReactElement, useCallback, useState } from "react"; +import styles from './styles.module.scss' +import { getMockSenderEnhancer } from "@rpldy/mock-sender"; + + + +const mockSenderEnhancer = getMockSenderEnhancer({ + delay: 1500, +}); + + +export interface ImageType { + id: string, + name: string, + url: string; +} + +type RenderPropArgs = { + isUploading?: boolean; + img: ImageType | null, + onAbort: () => void +} + +interface Props { + value: ImageType, + onChange: (new_value: ImageType | null) => void; + wrapperClass?: string; + render: (args: RenderPropArgs) => ReactElement; +} + + +export default function ScreenshotsInput(props: Props) { + + const { value, onChange, render } = props; + + + const [currentlyUploadingItem, setCurrentlyUploadingItem] = useState(null) + + + return ( + { + onChange(null) + + setCurrentlyUploadingItem({ + id: item.id, + url: URL.createObjectURL(item.file), + name: item.file.name, + }) + }, + [UPLOADER_EVENTS.ITEM_FINALIZE]: () => setCurrentlyUploadingItem(null), + [UPLOADER_EVENTS.ITEM_FINISH]: (item) => { + + // Just for mocking purposes + const dataUrl = URL.createObjectURL(item.file); + + const { id, filename, variants } = item?.uploadResponse?.data?.result ?? { + id: Math.random().toString(), + filename: item.file.name, + variants: [ + "", + dataUrl + ] + } + if (id) { + onChange({ id, name: filename, url: variants[1] }) + } + } + }} + > + + + ) +} + + +const DropZone = forwardRef((props, ref) => { + const { onClick, children, renderProps, ...buttonProps } = props; + + + + useRequestPreSend(async (data) => { + + const filename = data.items?.[0].file.name ?? '' + + // const url = await fetchUploadUrl({ filename }); + return { + options: { + destination: { + url: "URL" + } + } + } + }) + + const onZoneClick = useCallback( + (e: any) => { + if (onClick) { + onClick(e); + } + }, + [onClick] + ); + + return + + {renderProps.render({ + img: renderProps.img, + isUploading: renderProps.isUploading, + + })} + +}) + +const DropZoneButton = asUploadButton(DropZone); diff --git a/src/Components/Inputs/SingleImageUploadInput/styles.module.scss b/src/Components/Inputs/SingleImageUploadInput/styles.module.scss new file mode 100644 index 0000000..217ca39 --- /dev/null +++ b/src/Components/Inputs/SingleImageUploadInput/styles.module.scss @@ -0,0 +1,25 @@ +.zone { + background-color: #f2f4f7; + border-color: #e4e7ec; + + .active_content { + display: none; + } + + .idle_content { + display: block; + } + + &.active { + background-color: #b3a0ff; + border-color: #9e88ff; + + .active_content { + display: block; + } + + .idle_content { + display: none; + } + } +} From f7665d7ed1505821a7493e1f3361457a442d241d Mon Sep 17 00:00:00 2001 From: MTG2000 Date: Sun, 28 Aug 2022 12:44:39 +0300 Subject: [PATCH 06/20] feat: New InsertImageModal, various images types input (avatar/thumbnail/cover), use new images input in profile, story cover, story content --- .../Inputs/FilesInput/DropInput.jsx | 66 --------- .../Inputs/FilesInput/FileThumbnail.tsx | 75 ---------- .../Inputs/FilesInput/FilesDropInput.tsx | 88 ------------ .../Inputs/FilesInput/FilesInput.tsx | 136 ------------------ .../Inputs/FilesInput/FilesThumbnails.tsx | 29 ---- .../AvatarInput/AvatarInput.stories.tsx | 29 ++++ .../FilesInputs/AvatarInput/AvatarInput.tsx | 71 +++++++++ .../CoverImageInput.stories.tsx | 31 ++++ .../CoverImageInput/CoverImageInput.tsx | 66 +++++++++ .../FileUploadInput.stories.tsx | 0 .../FileUploadInput/FileUploadInput.tsx | 2 +- .../FileUploadInput/ImagePreviews.tsx | 0 .../FileUploadInput/styles.module.scss | 0 .../ScreenshotsInput/ImagePreviews.tsx | 0 .../ScreenshotsInput/ScreenshotThumbnail.tsx | 0 .../ScreenshotsInput.stories.tsx | 0 .../ScreenshotsInput/ScreenshotsInput.tsx | 0 .../ScreenshotsInput/styles.module.scss | 0 .../SingleImageUploadInput/ImagePreviews.tsx | 0 .../ScreenshotThumbnail.tsx | 0 .../SingleImageUploadInput.stories.tsx | 28 ++++ .../SingleImageUploadInput.tsx | 17 ++- .../SingleImageUploadInput/styles.module.scss | 0 .../ThumbnailInput/ThumbnailInput.stories.tsx | 29 ++++ .../ThumbnailInput/ThumbnailInput.tsx | 54 +++++++ .../fetch-upload-img-url.tsx | 0 .../SingleImageUploadInput.stories.tsx | 118 --------------- .../InsertImageModal.stories.tsx | 11 +- .../InsertImageModal/InsertImageModal.tsx | 61 +++++++- .../InsertImageModal/index.tsx | 0 .../Components/BountyForm/BountyForm.tsx | 5 +- .../DraftContainer.stories.tsx | 1 - .../DraftsContainer/DraftsContainer.tsx | 2 +- .../Components/QuestionForm/QuestionForm.tsx | 5 +- .../StoryForm/StoryForm.stories.tsx | 1 - .../Components/StoryForm/StoryForm.tsx | 33 +++-- .../CreateStoryPage/CreateStoryPage.tsx | 9 +- .../StoryPageContent/useUpdateStory.tsx | 2 +- .../BasicProfileInfoTab.tsx | 39 +++-- src/redux/features/modals.slice.ts | 2 +- src/services/notifications.service.ts | 4 +- 41 files changed, 436 insertions(+), 578 deletions(-) delete mode 100644 src/Components/Inputs/FilesInput/DropInput.jsx delete mode 100644 src/Components/Inputs/FilesInput/FileThumbnail.tsx delete mode 100644 src/Components/Inputs/FilesInput/FilesDropInput.tsx delete mode 100644 src/Components/Inputs/FilesInput/FilesInput.tsx delete mode 100644 src/Components/Inputs/FilesInput/FilesThumbnails.tsx create mode 100644 src/Components/Inputs/FilesInputs/AvatarInput/AvatarInput.stories.tsx create mode 100644 src/Components/Inputs/FilesInputs/AvatarInput/AvatarInput.tsx create mode 100644 src/Components/Inputs/FilesInputs/CoverImageInput/CoverImageInput.stories.tsx create mode 100644 src/Components/Inputs/FilesInputs/CoverImageInput/CoverImageInput.tsx rename src/Components/Inputs/{ => FilesInputs}/FileUploadInput/FileUploadInput.stories.tsx (100%) rename src/Components/Inputs/{ => FilesInputs}/FileUploadInput/FileUploadInput.tsx (98%) rename src/Components/Inputs/{ => FilesInputs}/FileUploadInput/ImagePreviews.tsx (100%) rename src/Components/Inputs/{ => FilesInputs}/FileUploadInput/styles.module.scss (100%) rename src/Components/Inputs/{ => FilesInputs}/ScreenshotsInput/ImagePreviews.tsx (100%) rename src/Components/Inputs/{ => FilesInputs}/ScreenshotsInput/ScreenshotThumbnail.tsx (100%) rename src/Components/Inputs/{ => FilesInputs}/ScreenshotsInput/ScreenshotsInput.stories.tsx (100%) rename src/Components/Inputs/{ => FilesInputs}/ScreenshotsInput/ScreenshotsInput.tsx (100%) rename src/Components/Inputs/{ => FilesInputs}/ScreenshotsInput/styles.module.scss (100%) rename src/Components/Inputs/{ => FilesInputs}/SingleImageUploadInput/ImagePreviews.tsx (100%) rename src/Components/Inputs/{ => FilesInputs}/SingleImageUploadInput/ScreenshotThumbnail.tsx (100%) create mode 100644 src/Components/Inputs/FilesInputs/SingleImageUploadInput/SingleImageUploadInput.stories.tsx rename src/Components/Inputs/{ => FilesInputs}/SingleImageUploadInput/SingleImageUploadInput.tsx (88%) rename src/Components/Inputs/{ => FilesInputs}/SingleImageUploadInput/styles.module.scss (100%) create mode 100644 src/Components/Inputs/FilesInputs/ThumbnailInput/ThumbnailInput.stories.tsx create mode 100644 src/Components/Inputs/FilesInputs/ThumbnailInput/ThumbnailInput.tsx rename src/Components/Inputs/{FileUploadInput => FilesInputs}/fetch-upload-img-url.tsx (100%) delete mode 100644 src/Components/Inputs/SingleImageUploadInput/SingleImageUploadInput.stories.tsx rename src/Components/{Inputs/TextEditor => Modals}/InsertImageModal/InsertImageModal.stories.tsx (68%) rename src/Components/{Inputs/TextEditor => Modals}/InsertImageModal/InsertImageModal.tsx (50%) rename src/Components/{Inputs/TextEditor => Modals}/InsertImageModal/index.tsx (100%) diff --git a/src/Components/Inputs/FilesInput/DropInput.jsx b/src/Components/Inputs/FilesInput/DropInput.jsx deleted file mode 100644 index 6a2b73c..0000000 --- a/src/Components/Inputs/FilesInput/DropInput.jsx +++ /dev/null @@ -1,66 +0,0 @@ -import { useToggle } from "@react-hookz/web"; -import React from "react"; -import { FileDrop } from "react-file-drop"; - -export default function DropInput({ - value: files, - onChange, - emptyContent, - draggingContent, - hasFilesContent, - height, - multiple = false, - allowedType = "*", - classes = { - base: "", - idle: "", - dragging: "", - }, -}) { - const [isDragging, toggleDrag] = useToggle(false); - const fileInputRef = React.useRef(null); - - const onAddFiles = (_files) => { - onChange(_files); - // do something with your files... - }; - - const uploadClick = () => { - fileInputRef.current.click(); - }; - - const status = isDragging ? "dragging" : files ? "has-files" : "empty"; - - return ( -
- onAddFiles(files)} - onTargetClick={uploadClick} - onFrameDragEnter={() => toggleDrag(true)} - onFrameDragLeave={() => toggleDrag(false)} - onFrameDrop={() => toggleDrag(false)} - className={`h-full cursor-pointer`} - targetClassName={`h-full ${classes.base} ${ - status === "empty" && classes.idle - }`} - draggingOverFrameClassName={`${classes.dragging}`} - > - {status === "dragging" && draggingContent} - {status === "empty" && emptyContent} - {status === "has-files" && hasFilesContent} - - onAddFiles(e.target.files)} - ref={fileInputRef} - type="file" - className="hidden" - multiple={multiple} - accept={allowedType} - /> -
- ); -} diff --git a/src/Components/Inputs/FilesInput/FileThumbnail.tsx b/src/Components/Inputs/FilesInput/FileThumbnail.tsx deleted file mode 100644 index b03fb5b..0000000 --- a/src/Components/Inputs/FilesInput/FileThumbnail.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { useMemo } from "react"; -import { MdClose } from "react-icons/md"; -import IconButton from "src/Components/IconButton/IconButton"; - -interface Props { - file: File | string, - onRemove?: () => void -} - -function getFileType(file: File | string) { - if (typeof file === 'string') { - if (/^http[^?]*.(jpg|jpeg|gif|png|tiff|bmp)(\?(.*))?$/gmi.test(file)) - return 'image' - if (/\.(pdf|doc|docx)$/.test(file)) - return 'document'; - - return 'unknown' - } - else { - if (file['type'].split('/')[0] === 'image') - return 'image' - - return 'unknown' - } -} - -type ThumbnailFile = { - name: string; - src: string; - type: ReturnType -} - -function processFile(file: Props['file']): ThumbnailFile { - - const fileType = getFileType(file); - - if (typeof file === 'string') return { name: file, src: file, type: fileType }; - - return { - name: file.name, - src: URL.createObjectURL(file), - type: fileType - }; - -} - - -export default function FileThumbnail({ file: f, onRemove }: Props) { - - const file = useMemo(() => processFile(f), [f]) - - return ( -
- -
- - - -
-
- ) -} diff --git a/src/Components/Inputs/FilesInput/FilesDropInput.tsx b/src/Components/Inputs/FilesInput/FilesDropInput.tsx deleted file mode 100644 index 6d96622..0000000 --- a/src/Components/Inputs/FilesInput/FilesDropInput.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { FaImage } from "react-icons/fa"; -import { UnionToObjectKeys } from "src/utils/types/utils"; -import DropInput from "./DropInput"; - - -type Props = { - height?: number - multiple?: boolean; - value?: File[] | string[] | string; - max?: number; - onBlur?: () => void; - onChange?: (files: (File | string)[] | null) => void - uploadBtn?: JSX.Element - uploadText?: string; - allowedType?: 'images'; - classes?: Partial<{ - base: string, - idle: string, - dragging: string, - hasFiles: string - }> -} - -const fileAccept: UnionToObjectKeys = { - images: ".png, .jpg, .jpeg" -} as const; - -const fileUrlToObject = async (url: string, fileName: string = 'filename') => { - const res = await fetch(url); - const contentType = res.headers.get('content-type') as string; - const blob = await res.blob() - const file = new File([blob], fileName, { contentType } as any) - return file -} - -export default function FilesInput({ - height = 200, - multiple, - value, - max = 3, - onBlur, - onChange, - allowedType = 'images', - classes, - ...props -}: Props) { - - - const baseClasses = classes?.base ?? 'p-32 rounded-8 text-center flex flex-col justify-center items-center' - const idleClasses = classes?.idle ?? 'bg-primary-50 hover:bg-primary-25 border border-dashed border-primary-500 text-gray-800' - const draggingClasses = classes?.dragging ?? 'bg-primary-500 text-white' - - return ( - - ) -} - -const defaultEmptyContent = ( - <> -
- {" "} - Drop your files here -
-

- or {" "} -

- -); - -const defaultDraggingContent =

Drop your files here ⬇⬇⬇

; - -const defaultHasFilesContent = ( -

Files Uploaded Successfully!!

-); \ No newline at end of file diff --git a/src/Components/Inputs/FilesInput/FilesInput.tsx b/src/Components/Inputs/FilesInput/FilesInput.tsx deleted file mode 100644 index 85e6cdb..0000000 --- a/src/Components/Inputs/FilesInput/FilesInput.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { createAction } from "@reduxjs/toolkit"; -import React, { ChangeEvent, useCallback, useRef } from "react" -import { BsUpload } from "react-icons/bs"; -import { FaImage } from "react-icons/fa"; -import Button from "src/Components/Button/Button" -import { openModal } from "src/redux/features/modals.slice"; -import { useAppDispatch } from "src/utils/hooks"; -import { useReduxEffect } from "src/utils/hooks/useReduxEffect"; -import { UnionToObjectKeys } from "src/utils/types/utils"; -import FilesThumbnails from "./FilesThumbnails"; - - -type Props = { - multiple?: boolean; - value?: File[] | string[] | string; - max?: number; - onBlur?: () => void; - onChange?: (files: (File | string)[] | null) => void - uploadBtn?: JSX.Element - uploadText?: string; - allowedType?: 'images'; -} - -const fileAccept: UnionToObjectKeys = { - images: ".png, .jpg, .jpeg" -} as const; - -const fileUrlToObject = async (url: string, fileName: string = 'filename') => { - const res = await fetch(url); - const contentType = res.headers.get('content-type') as string; - const blob = await res.blob() - const file = new File([blob], fileName, { contentType } as any) - return file -} - -const INSERT_IMAGE_ACTION = createAction<{ src: string, alt?: string }>('COVER_IMAGE_INSERTED')({ src: '', alt: "" }) - -const FilesInput = React.forwardRef(({ - multiple, - value, - max = 3, - onBlur, - onChange, - allowedType = 'images', - uploadText = 'Upload files', - ...props -}, ref) => { - - - const dispatch = useAppDispatch(); - - const handleClick = () => { - // ref.current.click(); - dispatch(openModal({ - Modal: "InsertImageModal", - props: { - callbackAction: { - type: INSERT_IMAGE_ACTION.type, - payload: { - src: "", - alt: "" - } - } - } - })) - } - - const onInsertImgUrl = useCallback(({ payload: { src, alt } }: typeof INSERT_IMAGE_ACTION) => { - if (typeof value === 'string') - onChange?.([value, src]); - else - onChange?.([...(value ?? []), src]); - }, [onChange, value]) - - useReduxEffect(onInsertImgUrl, INSERT_IMAGE_ACTION.type) - - const handleChange = (e: ChangeEvent) => { - const files = e.target.files && Array.from(e.target.files).slice(0, max); - if (typeof value === 'string') - onChange?.([value, ...(files ?? [])]); - else - onChange?.([...(value ?? []), ...(files ?? [])]); - } - - const handleRemove = async (idx: number) => { - if (!value) return onChange?.([]); - if (typeof value === 'string') - onChange?.([]); - else { - let files = [...value] - files.splice(idx, 1); - - //change all files urls to file objects - const filesConverted = await Promise.all(files.map(async file => { - if (typeof file === 'string') return await fileUrlToObject(file, "") - else return file; - })) - - onChange?.(filesConverted); - } - } - - const canUploadMore = multiple ? - !value || (value && value.length < max) - : - !value || value.length === 0 - - - const uploadBtn = props.uploadBtn ? - React.cloneElement(props.uploadBtn, { onClick: handleClick }) - : - - - return ( - <> - - { - canUploadMore && - <> - {uploadBtn} - - - } - - ) -}) - - -export default FilesInput; \ No newline at end of file diff --git a/src/Components/Inputs/FilesInput/FilesThumbnails.tsx b/src/Components/Inputs/FilesInput/FilesThumbnails.tsx deleted file mode 100644 index 362528e..0000000 --- a/src/Components/Inputs/FilesInput/FilesThumbnails.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { useMemo } from 'react' -import FileThumbnail from './FileThumbnail'; - -interface Props { - files?: (File | string)[] | string; - onRemove?: (idx: number) => void -} - -function processFiles(files: Props['files']) { - - if (!files) return []; - if (typeof files === 'string') return [files]; - return files; -} - -export default function FilesThumbnails({ files, onRemove }: Props) { - const filesConverted = useMemo(() => processFiles(files), [files]) - - return ( -
- { - filesConverted.map((file, idx) => onRemove?.(idx)} />) - } -
- ) -} diff --git a/src/Components/Inputs/FilesInputs/AvatarInput/AvatarInput.stories.tsx b/src/Components/Inputs/FilesInputs/AvatarInput/AvatarInput.stories.tsx new file mode 100644 index 0000000..73f451e --- /dev/null +++ b/src/Components/Inputs/FilesInputs/AvatarInput/AvatarInput.stories.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import AvatarInput from './AvatarInput'; +import { WrapFormController } from 'src/utils/storybook/decorators'; +import { ImageType } from '../SingleImageUploadInput/SingleImageUploadInput'; + +export default { + title: 'Shared/Inputs/Files Inputs/Avatar ', + component: AvatarInput, + decorators: [ + WrapFormController<{ avatar: ImageType | null }>({ + logValues: true, + name: "avatar", + defaultValues: { + avatar: null + } + })] +} as ComponentMeta; + +const Template: ComponentStory = (args, context) => { + + return + +} + + +export const Default = Template.bind({}); +Default.args = { +} \ No newline at end of file diff --git a/src/Components/Inputs/FilesInputs/AvatarInput/AvatarInput.tsx b/src/Components/Inputs/FilesInputs/AvatarInput/AvatarInput.tsx new file mode 100644 index 0000000..907d1ad --- /dev/null +++ b/src/Components/Inputs/FilesInputs/AvatarInput/AvatarInput.tsx @@ -0,0 +1,71 @@ +import React, { ComponentProps } from 'react' +import { CgArrowsExchangeV } from 'react-icons/cg'; +import { FiCamera } from 'react-icons/fi'; +import { IoMdClose } from 'react-icons/io'; +import { RotatingLines } from 'react-loader-spinner'; +import { Nullable } from 'remirror'; +import SingleImageUploadInput from '../SingleImageUploadInput/SingleImageUploadInput' + +type Value = ComponentProps['value'] + +interface Props { + width?: number; + isRemovable?: boolean + value: Value; + onChange: (new_value: Nullable) => void +} + +export default function AvatarInput(props: Props) { + return ( +
+ +
+ {!img && +
+

+
+ Add Image +
+
} + {img && + <> + + {!isUploading && +
+ + {props.isRemovable && } +
+ } + } + {isUploading && +
+ +
+ } +
} + /> +
+ + ) +} diff --git a/src/Components/Inputs/FilesInputs/CoverImageInput/CoverImageInput.stories.tsx b/src/Components/Inputs/FilesInputs/CoverImageInput/CoverImageInput.stories.tsx new file mode 100644 index 0000000..9bedfe5 --- /dev/null +++ b/src/Components/Inputs/FilesInputs/CoverImageInput/CoverImageInput.stories.tsx @@ -0,0 +1,31 @@ +import React from 'react' +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { WrapFormController } from 'src/utils/storybook/decorators'; +import { ImageType } from '../SingleImageUploadInput/SingleImageUploadInput'; +import CoverImageInput from './CoverImageInput'; + +export default { + title: 'Shared/Inputs/Files Inputs/Cover Image ', + component: CoverImageInput, + decorators: [ + WrapFormController<{ thumbnail: ImageType | null }>({ + logValues: true, + name: "thumbnail", + defaultValues: { + thumbnail: null + } + })] +} as ComponentMeta; + +const Template: ComponentStory = (args, context) => { + + return
+ +
+ +} + + +export const Default = Template.bind({}); +Default.args = { +} \ No newline at end of file diff --git a/src/Components/Inputs/FilesInputs/CoverImageInput/CoverImageInput.tsx b/src/Components/Inputs/FilesInputs/CoverImageInput/CoverImageInput.tsx new file mode 100644 index 0000000..4fc16c9 --- /dev/null +++ b/src/Components/Inputs/FilesInputs/CoverImageInput/CoverImageInput.tsx @@ -0,0 +1,66 @@ +import React, { ComponentProps } from 'react' +import { FaImage } from 'react-icons/fa'; +import { CgArrowsExchangeV } from 'react-icons/cg'; +import { IoMdClose } from 'react-icons/io'; +import { RotatingLines } from 'react-loader-spinner'; +import { Nullable } from 'remirror'; +import SingleImageUploadInput from '../SingleImageUploadInput/SingleImageUploadInput' + +type Value = ComponentProps['value'] + +interface Props { + value: Value; + rounded?: string; + onChange: (new_value: Nullable) => void +} + +export default function CoverImageInput(props: Props) { + return ( +
+ +
+ {!img &&
+

+
+ Drop a COVER IMAGE here or
Click to browse +
+
} + {img && <> + + {!isUploading && +
+ + + +
+ } + } + {isUploading && +
+ +
+ } +
} + /> +
+ + ) +} diff --git a/src/Components/Inputs/FileUploadInput/FileUploadInput.stories.tsx b/src/Components/Inputs/FilesInputs/FileUploadInput/FileUploadInput.stories.tsx similarity index 100% rename from src/Components/Inputs/FileUploadInput/FileUploadInput.stories.tsx rename to src/Components/Inputs/FilesInputs/FileUploadInput/FileUploadInput.stories.tsx diff --git a/src/Components/Inputs/FileUploadInput/FileUploadInput.tsx b/src/Components/Inputs/FilesInputs/FileUploadInput/FileUploadInput.tsx similarity index 98% rename from src/Components/Inputs/FileUploadInput/FileUploadInput.tsx rename to src/Components/Inputs/FilesInputs/FileUploadInput/FileUploadInput.tsx index 241a317..ebfef3c 100644 --- a/src/Components/Inputs/FileUploadInput/FileUploadInput.tsx +++ b/src/Components/Inputs/FilesInputs/FileUploadInput/FileUploadInput.tsx @@ -1,7 +1,7 @@ import Uploady, { useUploady, useRequestPreSend, UPLOADER_EVENTS, } from "@rpldy/uploady"; import { asUploadButton } from "@rpldy/upload-button"; import Button from "src/Components/Button/Button"; -import { fetchUploadUrl } from "./fetch-upload-img-url"; +import { fetchUploadUrl } from "../fetch-upload-img-url"; import ImagePreviews from "./ImagePreviews"; import { FaImage } from "react-icons/fa"; import UploadDropZone from "@rpldy/upload-drop-zone"; diff --git a/src/Components/Inputs/FileUploadInput/ImagePreviews.tsx b/src/Components/Inputs/FilesInputs/FileUploadInput/ImagePreviews.tsx similarity index 100% rename from src/Components/Inputs/FileUploadInput/ImagePreviews.tsx rename to src/Components/Inputs/FilesInputs/FileUploadInput/ImagePreviews.tsx diff --git a/src/Components/Inputs/FileUploadInput/styles.module.scss b/src/Components/Inputs/FilesInputs/FileUploadInput/styles.module.scss similarity index 100% rename from src/Components/Inputs/FileUploadInput/styles.module.scss rename to src/Components/Inputs/FilesInputs/FileUploadInput/styles.module.scss diff --git a/src/Components/Inputs/ScreenshotsInput/ImagePreviews.tsx b/src/Components/Inputs/FilesInputs/ScreenshotsInput/ImagePreviews.tsx similarity index 100% rename from src/Components/Inputs/ScreenshotsInput/ImagePreviews.tsx rename to src/Components/Inputs/FilesInputs/ScreenshotsInput/ImagePreviews.tsx diff --git a/src/Components/Inputs/ScreenshotsInput/ScreenshotThumbnail.tsx b/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotThumbnail.tsx similarity index 100% rename from src/Components/Inputs/ScreenshotsInput/ScreenshotThumbnail.tsx rename to src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotThumbnail.tsx diff --git a/src/Components/Inputs/ScreenshotsInput/ScreenshotsInput.stories.tsx b/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotsInput.stories.tsx similarity index 100% rename from src/Components/Inputs/ScreenshotsInput/ScreenshotsInput.stories.tsx rename to src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotsInput.stories.tsx diff --git a/src/Components/Inputs/ScreenshotsInput/ScreenshotsInput.tsx b/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotsInput.tsx similarity index 100% rename from src/Components/Inputs/ScreenshotsInput/ScreenshotsInput.tsx rename to src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotsInput.tsx diff --git a/src/Components/Inputs/ScreenshotsInput/styles.module.scss b/src/Components/Inputs/FilesInputs/ScreenshotsInput/styles.module.scss similarity index 100% rename from src/Components/Inputs/ScreenshotsInput/styles.module.scss rename to src/Components/Inputs/FilesInputs/ScreenshotsInput/styles.module.scss diff --git a/src/Components/Inputs/SingleImageUploadInput/ImagePreviews.tsx b/src/Components/Inputs/FilesInputs/SingleImageUploadInput/ImagePreviews.tsx similarity index 100% rename from src/Components/Inputs/SingleImageUploadInput/ImagePreviews.tsx rename to src/Components/Inputs/FilesInputs/SingleImageUploadInput/ImagePreviews.tsx diff --git a/src/Components/Inputs/SingleImageUploadInput/ScreenshotThumbnail.tsx b/src/Components/Inputs/FilesInputs/SingleImageUploadInput/ScreenshotThumbnail.tsx similarity index 100% rename from src/Components/Inputs/SingleImageUploadInput/ScreenshotThumbnail.tsx rename to src/Components/Inputs/FilesInputs/SingleImageUploadInput/ScreenshotThumbnail.tsx diff --git a/src/Components/Inputs/FilesInputs/SingleImageUploadInput/SingleImageUploadInput.stories.tsx b/src/Components/Inputs/FilesInputs/SingleImageUploadInput/SingleImageUploadInput.stories.tsx new file mode 100644 index 0000000..06e9fe5 --- /dev/null +++ b/src/Components/Inputs/FilesInputs/SingleImageUploadInput/SingleImageUploadInput.stories.tsx @@ -0,0 +1,28 @@ +import React from 'react' +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import SingleImageUploadInput, { ImageType } from './SingleImageUploadInput'; +import { WrapFormController } from 'src/utils/storybook/decorators'; +import { RotatingLines } from 'react-loader-spinner'; +import { FiCamera, } from 'react-icons/fi'; +import { FaExchangeAlt, FaImage } from 'react-icons/fa'; + +export default { + title: 'Shared/Inputs/Files Inputs/Single Image Upload ', + component: SingleImageUploadInput, + decorators: [ + WrapFormController<{ avatar: ImageType | null }>({ + logValues: true, + name: "avatar", + defaultValues: { + avatar: null + } + })] +} as ComponentMeta; + +const Template: ComponentStory = (args, context) => { + + return + +} + + diff --git a/src/Components/Inputs/SingleImageUploadInput/SingleImageUploadInput.tsx b/src/Components/Inputs/FilesInputs/SingleImageUploadInput/SingleImageUploadInput.tsx similarity index 88% rename from src/Components/Inputs/SingleImageUploadInput/SingleImageUploadInput.tsx rename to src/Components/Inputs/FilesInputs/SingleImageUploadInput/SingleImageUploadInput.tsx index f95cd31..57218e6 100644 --- a/src/Components/Inputs/SingleImageUploadInput/SingleImageUploadInput.tsx +++ b/src/Components/Inputs/FilesInputs/SingleImageUploadInput/SingleImageUploadInput.tsx @@ -5,17 +5,19 @@ import UploadDropZone from "@rpldy/upload-drop-zone"; import { forwardRef, ReactElement, useCallback, useState } from "react"; import styles from './styles.module.scss' import { getMockSenderEnhancer } from "@rpldy/mock-sender"; +import { NotificationsService } from "src/services"; const mockSenderEnhancer = getMockSenderEnhancer({ delay: 1500, + }); export interface ImageType { - id: string, - name: string, + id?: string, + name?: string, url: string; } @@ -26,14 +28,14 @@ type RenderPropArgs = { } interface Props { - value: ImageType, + value: ImageType | null | undefined, onChange: (new_value: ImageType | null) => void; wrapperClass?: string; render: (args: RenderPropArgs) => ReactElement; } -export default function ScreenshotsInput(props: Props) { +export default function SingleImageUploadInput(props: Props) { const { value, onChange, render } = props; @@ -43,6 +45,7 @@ export default function ScreenshotsInput(props: Props) { return ( { + NotificationsService.error("An error happened while uploading. Please try again.") + }, [UPLOADER_EVENTS.ITEM_FINALIZE]: () => setCurrentlyUploadingItem(null), [UPLOADER_EVENTS.ITEM_FINISH]: (item) => { @@ -80,7 +86,7 @@ export default function ScreenshotsInput(props: Props) { extraProps={{ renderProps: { isUploading: !!currentlyUploadingItem, - img: currentlyUploadingItem || value || null, + img: currentlyUploadingItem ?? value ?? null, render, wrapperClass: props.wrapperClass } @@ -123,6 +129,7 @@ const DropZone = forwardRef((props, ref) => { return ({ + logValues: true, + name: "thumbnail", + defaultValues: { + thumbnail: null + } + })] +} as ComponentMeta; + +const Template: ComponentStory = (args, context) => { + + return + +} + + +export const Default = Template.bind({}); +Default.args = { +} \ No newline at end of file diff --git a/src/Components/Inputs/FilesInputs/ThumbnailInput/ThumbnailInput.tsx b/src/Components/Inputs/FilesInputs/ThumbnailInput/ThumbnailInput.tsx new file mode 100644 index 0000000..1b677fa --- /dev/null +++ b/src/Components/Inputs/FilesInputs/ThumbnailInput/ThumbnailInput.tsx @@ -0,0 +1,54 @@ +import React, { ComponentProps } from 'react' +import { FiCamera } from 'react-icons/fi'; +import { RotatingLines } from 'react-loader-spinner'; +import { Nullable } from 'remirror'; +import SingleImageUploadInput from '../SingleImageUploadInput/SingleImageUploadInput' + +type Value = ComponentProps['value'] + +interface Props { + width?: number + value: Value; + onChange: (new_value: Nullable) => void +} + +export default function ThumbnailInput(props: Props) { + return ( +
+
+ {img && } + {!img && + <> +

+
+ Add Image +
+ } + {isUploading && +
+ +
+ } +
} + /> +
+ + ) +} diff --git a/src/Components/Inputs/FileUploadInput/fetch-upload-img-url.tsx b/src/Components/Inputs/FilesInputs/fetch-upload-img-url.tsx similarity index 100% rename from src/Components/Inputs/FileUploadInput/fetch-upload-img-url.tsx rename to src/Components/Inputs/FilesInputs/fetch-upload-img-url.tsx diff --git a/src/Components/Inputs/SingleImageUploadInput/SingleImageUploadInput.stories.tsx b/src/Components/Inputs/SingleImageUploadInput/SingleImageUploadInput.stories.tsx deleted file mode 100644 index e2d9061..0000000 --- a/src/Components/Inputs/SingleImageUploadInput/SingleImageUploadInput.stories.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import React from 'react' -import { ComponentStory, ComponentMeta } from '@storybook/react'; -import SingleImageUploadInput, { ImageType } from './SingleImageUploadInput'; -import { WrapFormController } from 'src/utils/storybook/decorators'; -import { RotatingLines } from 'react-loader-spinner'; -import { FiCamera, } from 'react-icons/fi'; -import { FaExchangeAlt, FaImage } from 'react-icons/fa'; - -export default { - title: 'Shared/Inputs/Files Inputs/Single Image Upload ', - component: SingleImageUploadInput, - decorators: [ - WrapFormController<{ avatar: ImageType | null }>({ - logValues: true, - name: "avatar", - defaultValues: { - avatar: null - } - })] -} as ComponentMeta; - -const Template: ComponentStory = (args, context) => { - - return - -} - - -export const Avatar = Template.bind({}); -Avatar.args = { - wrapperClass: "inline-block cursor-pointer ", - render: ({ img, isUploading }) =>
- {img && } - {!img && - <> -

-
- Add Image -
- } - {isUploading && -
- -
- } -
-} - -export const Thumbnail = Template.bind({}); -Thumbnail.args = { - wrapperClass: "inline-block cursor-pointer ", - render: ({ img, isUploading }) =>
- {img && } - {!img && - <> -

-
- Add Image -
- } - {isUploading && -
- -
- } -
-} - -export const Cover = Template.bind({}); -Cover.args = { - wrapperClass: "block cursor-pointer ", - render: ({ img, isUploading }) =>
- {img && <> - - {!isUploading && - } - } - {!img && - <> -

-
- Drop a COVER IMAGE here or
Click to browse -
- } - {isUploading && -
- -
- } -
-} - diff --git a/src/Components/Inputs/TextEditor/InsertImageModal/InsertImageModal.stories.tsx b/src/Components/Modals/InsertImageModal/InsertImageModal.stories.tsx similarity index 68% rename from src/Components/Inputs/TextEditor/InsertImageModal/InsertImageModal.stories.tsx rename to src/Components/Modals/InsertImageModal/InsertImageModal.stories.tsx index ed4f283..b5d7197 100644 --- a/src/Components/Inputs/TextEditor/InsertImageModal/InsertImageModal.stories.tsx +++ b/src/Components/Modals/InsertImageModal/InsertImageModal.stories.tsx @@ -5,7 +5,7 @@ import InsertImageModal from './InsertImageModal'; import { ModalsDecorator } from 'src/utils/storybook/decorators'; export default { - title: 'Shared/Inputs/Text Editor/Insert Image Modal', + title: 'Shared/Inputs/Files Inputs/Image Modal', component: InsertImageModal, decorators: [ModalsDecorator] @@ -14,4 +14,13 @@ export default { const Template: ComponentStory = (args) => ; export const Default = Template.bind({}); +Default.args = { + callbackAction: { + type: "INSERT_IMAGE_IN_STORY", + payload: { + src: "", + alt: "", + } + } +} diff --git a/src/Components/Inputs/TextEditor/InsertImageModal/InsertImageModal.tsx b/src/Components/Modals/InsertImageModal/InsertImageModal.tsx similarity index 50% rename from src/Components/Inputs/TextEditor/InsertImageModal/InsertImageModal.tsx rename to src/Components/Modals/InsertImageModal/InsertImageModal.tsx index 0852dea..d3b09d0 100644 --- a/src/Components/Inputs/TextEditor/InsertImageModal/InsertImageModal.tsx +++ b/src/Components/Modals/InsertImageModal/InsertImageModal.tsx @@ -5,6 +5,9 @@ import { IoClose } from 'react-icons/io5' import Button from 'src/Components/Button/Button' import { useAppDispatch } from 'src/utils/hooks' import { PayloadAction } from '@reduxjs/toolkit' +import { RotatingLines } from 'react-loader-spinner' +import { FaExchangeAlt, FaImage } from 'react-icons/fa' +import SingleImageUploadInput, { ImageType } from 'src/Components/Inputs/FilesInputs/SingleImageUploadInput/SingleImageUploadInput' interface Props extends ModalCard { callbackAction: PayloadAction<{ src: string, alt?: string }> @@ -12,16 +15,18 @@ interface Props extends ModalCard { export default function InsertImageModal({ onClose, direction, callbackAction, ...props }: Props) { - const [urlInput, setUrlInput] = useState("") + const [uploadedImage, setUploadedImage] = useState(null) const [altInput, setAltInput] = useState("") const dispatch = useAppDispatch(); const handleSubmit = (e: FormEvent) => { e.preventDefault() - if (urlInput.length > 10) { + console.log(uploadedImage?.url); + + if (uploadedImage?.url) { // onInsert({ src: urlInput, alt: altInput }) const action = Object.assign({}, callbackAction); - action.payload = { src: urlInput, alt: altInput } + action.payload = { src: uploadedImage.url, alt: altInput } dispatch(action) onClose?.(); } @@ -39,7 +44,7 @@ export default function InsertImageModal({ onClose, direction, callbackAction, .

Add Image

-
+ {/*

Image URL @@ -75,6 +80,54 @@ export default function InsertImageModal({ onClose, direction, callbackAction, . className='w-full h-full object-cover rounded-10' alt={altInput} />} +

*/} +
+ {img && <> + + {!isUploading && + } + } + {!img && + <> +

+
+ Drop an IMAGE here or
Click to browse +
+ } + {isUploading && +
+ +
+ } +
} + /> +
+

+ Alternative Text +

+
+ setAltInput(e.target.value)} + placeholder='A description for the content of this image' + /> +