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()} />
-    </>
-  )
-}