diff --git a/src/components/Inputs/ImageUpload.tsx b/src/components/Inputs/ImageUpload.tsx new file mode 100644 index 0000000..717183a --- /dev/null +++ b/src/components/Inputs/ImageUpload.tsx @@ -0,0 +1,116 @@ +import { useDropzone } from 'react-dropzone' +import React, { useCallback, useMemo, useState } from 'react' +import { + MediaOption, + MEDIA_OPTIONS, + ImageController, + MEDIA_DROPZONE_OPTIONS +} from '../../controllers' +import { errorFeedback } from '../../types' +import { MediaInputPopover } from './MediaInputPopover' + +export interface ImageUploadProps { + multiple?: boolean | undefined + onChange: (values: string[]) => void +} +export const ImageUpload = React.memo( + ({ multiple = false, onChange }: ImageUploadProps) => { + const [mediaOption, setMediaOption] = useState( + MEDIA_OPTIONS[0] + ) + const handleOptionChange = useCallback( + (mo: MediaOption) => () => { + setMediaOption(mo) + }, + [] + ) + const handleUpload = useCallback( + async (acceptedFiles: File[]) => { + if (acceptedFiles.length) { + try { + const imageController = new ImageController(mediaOption) + const urls: string[] = [] + for (let i = 0; i < acceptedFiles.length; i++) { + const file = acceptedFiles[i] + urls.push(await imageController.post(file)) + } + onChange(urls) + } catch (error) { + errorFeedback(error) + } + } + }, + [mediaOption, onChange] + ) + const { + getRootProps, + getInputProps, + isDragActive, + acceptedFiles, + isFileDialogActive, + isDragAccept, + isDragReject, + fileRejections + } = useDropzone({ + ...MEDIA_DROPZONE_OPTIONS, + onDrop: handleUpload, + multiple: multiple + }) + + const dropzoneLabel = useMemo( + () => + isFileDialogActive + ? 'Select files in dialog' + : isDragActive + ? isDragAccept + ? 'Drop the files here...' + : isDragReject + ? 'Drop the files here (one more more unsupported types)...' + : 'TODO' + : 'Click or drag files here', + [isDragAccept, isDragActive, isDragReject, isFileDialogActive] + ) + + return ( +
+ +
+ + + {dropzoneLabel} +
e.stopPropagation()} + > +
+ +
+ {MEDIA_OPTIONS.map((mo) => { + return ( +
+ {mo.name} +
+ ) + })} +
+
+
+
+
+ ) + } +) diff --git a/src/components/Inputs/MediaInputPopover.tsx b/src/components/Inputs/MediaInputPopover.tsx index 1f98de6..062ca4b 100644 --- a/src/components/Inputs/MediaInputPopover.tsx +++ b/src/components/Inputs/MediaInputPopover.tsx @@ -1,4 +1,5 @@ import * as Popover from '@radix-ui/react-popover' +import { v4 as uuidv4 } from 'uuid' import { useMemo } from 'react' import { FileRejection, FileWithPath } from 'react-dropzone' import { MediaInputError } from './MediaInputError' @@ -6,16 +7,15 @@ import { InputSuccess } from './Success' import styles from './MediaInputPopover.module.scss' interface MediaInputPopoverProps { - name: string acceptedFiles: readonly FileWithPath[] fileRejections: readonly FileRejection[] } export const MediaInputPopover = ({ - name, acceptedFiles, fileRejections }: MediaInputPopoverProps) => { + const uuid = useMemo(() => uuidv4(), []) const acceptedFileItems = useMemo( () => acceptedFiles.map((file) => ( @@ -27,7 +27,7 @@ export const MediaInputPopover = ({ [acceptedFiles] ) const fileRejectionItems = useMemo(() => { - const id = `${name}-errors` + const id = `errors-${uuid}` return (
) - }, [fileRejections, name]) + }, [fileRejections, uuid]) if (acceptedFiles.length === 0 && fileRejections.length === 0) return null diff --git a/src/components/Inputs/index.tsx b/src/components/Inputs/index.tsx index 61c6774..1658d59 100644 --- a/src/components/Inputs/index.tsx +++ b/src/components/Inputs/index.tsx @@ -1,15 +1,7 @@ -import React, { useCallback, useMemo, useState } from 'react' -import { useDropzone } from 'react-dropzone' -import { - ImageController, - MEDIA_DROPZONE_OPTIONS, - MEDIA_OPTIONS, - MediaOption -} from '../../controllers' -import '../../styles/styles.css' -import { errorFeedback } from '../../types' +import React, { useCallback } from 'react' import { InputError } from './Error' -import { MediaInputPopover } from './MediaInputPopover' +import { ImageUpload } from './ImageUpload' +import '../../styles/styles.css' interface InputFieldProps { label: string | React.ReactElement @@ -153,83 +145,40 @@ export const CheckboxFieldUncontrolled = ({
) -interface InputFieldWithImageUpload { +interface InputFieldWithImageUploadProps { label: string | React.ReactElement description?: string - multiple?: boolean | undefined placeholder: string name: string inputMode?: 'url' value: string error?: string - onChange: (name: string, value: string) => void + onInputChange: (name: string, value: string) => void } + export const InputFieldWithImageUpload = React.memo( ({ label, description, - multiple = false, placeholder, name, inputMode, value, error, - onChange - }: InputFieldWithImageUpload) => { - const [mediaOption, setMediaOption] = useState( - MEDIA_OPTIONS[0] - ) - const handleOptionChange = useCallback( - (mo: MediaOption) => () => { - setMediaOption(mo) - }, - [] - ) + onInputChange + }: InputFieldWithImageUploadProps) => { const handleChange = useCallback( (e: React.ChangeEvent) => { - onChange(name, e.target.value) + onInputChange(name, e.currentTarget.value) }, - [name, onChange] + [name, onInputChange] ) - const handleUpload = useCallback( - async (acceptedFiles: File[]) => { - try { - const imageController = new ImageController(mediaOption) - const url = await imageController.post(acceptedFiles[0]) - onChange(name, url) - } catch (error) { - errorFeedback(error) - } - }, - [mediaOption, name, onChange] - ) - const { - getRootProps, - getInputProps, - isDragActive, - acceptedFiles, - isFileDialogActive, - isDragAccept, - isDragReject, - fileRejections - } = useDropzone({ - ...MEDIA_DROPZONE_OPTIONS, - onDrop: handleUpload, - multiple: multiple - }) - const dropzoneLabel = useMemo( - () => - isFileDialogActive - ? 'Select files in dialog' - : isDragActive - ? isDragAccept - ? 'Drop the files here...' - : isDragReject - ? 'Drop the files here (one more more unsupported types)...' - : 'TODO' - : 'Click or drag files here', - [isDragAccept, isDragActive, isDragReject, isFileDialogActive] + const handleFileChange = useCallback( + (values: string[]) => { + onInputChange(name, values[0]) + }, + [name, onInputChange] ) return ( @@ -239,50 +188,8 @@ export const InputFieldWithImageUpload = React.memo(

{description}

)} -
- -
- + - {dropzoneLabel} -
e.stopPropagation()} - > -
- -
- {MEDIA_OPTIONS.map((mo) => { - return ( -
- {mo.name} -
- ) - })} -
-
-
-
-
{ const [formState, setFormState] = useState( initializeFormState(mod) ) + console.log(`[debug] screenshots`, formState.screenshotsUrls) const editorRef = useRef(null) useEffect(() => { @@ -230,7 +232,7 @@ export const ModForm = () => { name='featuredImageUrl' value={formState.featuredImageUrl} error={formErrors?.featuredImageUrl} - onChange={handleInputChange} + onInputChange={handleInputChange} /> {

- We recommend to upload images to https://nostr.build/ + We recommend to upload images to {MEDIA_OPTIONS[0].host}

+ + { + // values.forEach((screenshotUrl) => addScreenshotUrl()) + setFormState((prevState) => ({ + ...prevState, + screenshotsUrls: Array.from( + new Set([ + ...prevState.screenshotsUrls.filter((url) => url), + ...values + ]) + ) + })) + }} + /> + {formState.screenshotsUrls.map((url, index) => ( diff --git a/src/pages/write/index.tsx b/src/pages/write/index.tsx index 36d415e..9f21d31 100644 --- a/src/pages/write/index.tsx +++ b/src/pages/write/index.tsx @@ -108,7 +108,7 @@ export const WritePage = () => { name='featuredImageUrl' value={featuredImageUrl} error={formErrors?.image} - onChange={(_, value) => setfeaturedImageUrl(value)} + onInputChange={(_, value) => setfeaturedImageUrl(value)} />