From 7043b4f1d1db1e573a674134a011c7e6e13a7bbe Mon Sep 17 00:00:00 2001 From: ekzyis <ek@stacker.news> Date: Thu, 26 Oct 2023 16:22:26 +0200 Subject: [PATCH] refactor: Unify <ImageUpload> and <Upload> component --- components/avatar.js | 40 ++++++++++++------- components/form.js | 3 +- components/image.js | 17 +++++---- components/upload.js | 91 -------------------------------------------- 4 files changed, 39 insertions(+), 112 deletions(-) delete mode 100644 components/upload.js diff --git a/components/avatar.js b/components/avatar.js index e9d08c32..5c18f2e1 100644 --- a/components/avatar.js +++ b/components/avatar.js @@ -2,10 +2,10 @@ import { useRef, useState } from 'react' import AvatarEditor from 'react-avatar-editor' import Button from 'react-bootstrap/Button' import BootstrapForm from 'react-bootstrap/Form' -import Upload from './upload' import EditImage from '../svgs/image-edit-fill.svg' import Moon from '../svgs/moon-fill.svg' import { useShowModal } from './modal' +import { ImageUpload } from './image' export default function Avatar ({ onSuccess }) { const [uploading, setUploading] = useState() @@ -49,27 +49,41 @@ export default function Avatar ({ onSuccess }) { } return ( - <Upload - as={({ onClick }) => - <div className='position-absolute p-1 bg-dark pointer' onClick={onClick} style={{ bottom: '0', right: '0' }}> - {uploading - ? <Moon className='fill-white spin' /> - : <EditImage className='fill-white' />} - </div>} + <ImageUpload + avatar onError={e => { console.log(e) setUploading(false) }} onSelect={(file, upload) => { - showModal(onClose => <Body onClose={onClose} file={file} upload={upload} />) + return new Promise((resolve, reject) => + showModal(onClose => ( + <Body + onClose={() => { + onClose() + resolve() + }} + file={file} + upload={async (blob) => { + await upload(blob) + resolve(blob) + }} + /> + ))) }} - onSuccess={async key => { - onSuccess && onSuccess(key) + onSuccess={({ id }) => { + onSuccess?.(id) setUploading(false) }} - onStarted={() => { + onUpload={() => { setUploading(true) }} - /> + > + <div className='position-absolute p-1 bg-dark pointer' style={{ bottom: '0', right: '0' }}> + {uploading + ? <Moon className='fill-white spin' /> + : <EditImage className='fill-white' />} + </div> + </ImageUpload> ) } diff --git a/components/form.js b/components/form.js index 02ec6e21..7f9f4194 100644 --- a/components/form.js +++ b/components/form.js @@ -259,9 +259,10 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe </Nav.Item> <span className='ms-auto text-muted d-flex align-items-center'> <ImageUpload + multiple ref={imageUploadRef} className='d-flex align-items-center me-1' - onSelect={file => { + onUpload={file => { let text = innerRef.current.value if (text) text += '\n\n' text += `![Uploading ${file.name}…]()` diff --git a/components/image.js b/components/image.js index 30506771..f637d2cf 100644 --- a/components/image.js +++ b/components/image.js @@ -1,5 +1,5 @@ import styles from './text.module.css' -import { Fragment, useState, useEffect, useMemo, useCallback, forwardRef } from 'react' +import { Fragment, useState, useEffect, useMemo, useCallback, forwardRef, useRef } from 'react' import { IMGPROXY_URL_REGEXP } from '../lib/url' import { useShowModal } from './modal' import { useMe } from './me' @@ -137,13 +137,14 @@ export default function ZoomableImage ({ src, srcSet, ...props }) { return <ImageOriginal src={originalUrl} onClick={handleClick} {...props} /> } -export const ImageUpload = forwardRef(({ children, className, onSelect, onSuccess, onError }, ref) => { +export const ImageUpload = forwardRef(({ children, className, onSelect, onUpload, onSuccess, onError, multiple, avatar }, ref) => { const toaster = useToast() + ref ??= useRef(null) const [getSignedPOST] = useMutation( gql` - mutation getSignedPOST($type: String!, $size: Int!, $width: Int!, $height: Int!) { - getSignedPOST(type: $type, size: $size, width: $width, height: $height) { + mutation getSignedPOST($type: String!, $size: Int!, $width: Int!, $height: Int!, $avatar: Boolean) { + getSignedPOST(type: $type, size: $size, width: $width, height: $height, avatar: $avatar) { url fields } @@ -154,9 +155,10 @@ export const ImageUpload = forwardRef(({ children, className, onSelect, onSucces img.src = window.URL.createObjectURL(file) return new Promise((resolve, reject) => { img.onload = async () => { - onSelect?.(file) + onUpload?.(file) let data const variables = { + avatar, type: file.type, size: file.size, width: img.width, @@ -206,7 +208,7 @@ export const ImageUpload = forwardRef(({ children, className, onSelect, onSucces <input ref={ref} type='file' - multiple + multiple={multiple} className='d-none' accept={UPLOAD_TYPES_ALLOW.join(', ')} onChange={async (e) => { @@ -216,7 +218,8 @@ export const ImageUpload = forwardRef(({ children, className, onSelect, onSucces toaster.danger(`image must be ${UPLOAD_TYPES_ALLOW.map(t => t.replace('image/', '')).join(', ')}`) continue } - await s3Upload(file) + if (onSelect) await onSelect?.(file, s3Upload) + else await s3Upload(file) // TODO find out if this is needed and if so, why (copied from components/upload.js) e.target.value = null } diff --git a/components/upload.js b/components/upload.js deleted file mode 100644 index 122a26cb..00000000 --- a/components/upload.js +++ /dev/null @@ -1,91 +0,0 @@ -import { useRef } from 'react' -import { gql, useMutation } from '@apollo/client' -import { UPLOAD_TYPES_ALLOW } from '../lib/constants' - -export default function Upload ({ as: Component, onSelect, onStarted, onError, onSuccess }) { - const [getSignedPOST] = useMutation( - gql` - mutation getSignedPOST($type: String!, $size: Int!, $width: Int!, $height: Int!, $avatar: Boolean!) { - getSignedPOST(type: $type, size: $size, width: $width, height: $height, avatar: $avatar) { - url - fields - } - }`) - const ref = useRef() - - const upload = file => { - onStarted && onStarted() - - const img = new window.Image() - img.src = window.URL.createObjectURL(file) - img.onload = async () => { - let data - try { - ({ data } = await getSignedPOST({ - variables: { - avatar: true, - type: file.type, - size: file.size, - width: img.width, - height: img.height - } - })) - } catch (e) { - onError && onError(e.toString()) - return - } - - const form = new FormData() - Object.keys(data.getSignedPOST.fields).forEach(key => - form.append(key, data.getSignedPOST.fields[key])) - form.append('Content-Type', file.type) - form.append('Cache-Control', 'max-age=31536000') - form.append('acl', 'public-read') - form.append('file', file) - - const res = await fetch(data.getSignedPOST.url, { - method: 'POST', - body: form - }) - - if (!res.ok) { - onError && onError(res.statusText) - return - } - - onSuccess && onSuccess(data.getSignedPOST.fields.key) - } - } - - return ( - <> - <input - ref={ref} - type='file' - className='d-none' - accept={UPLOAD_TYPES_ALLOW.join(', ')} - onChange={(e) => { - if (e.target.files.length === 0) { - return - } - - const file = e.target.files[0] - - if (UPLOAD_TYPES_ALLOW.indexOf(file.type) === -1) { - onError && onError(`image must be ${UPLOAD_TYPES_ALLOW.map(t => t.replace('image/', '')).join(', ')}`) - return - } - - if (onSelect) { - onSelect(file, upload) - } else { - upload(file) - } - - e.target.value = null - }} - /> - <Component onClick={() => ref.current?.click()} /> - </> - ) -}