refactor: Unify <ImageUpload> and <Upload> component

This commit is contained in:
ekzyis 2023-10-26 16:22:26 +02:00
parent 1708c77458
commit 7043b4f1d1
4 changed files with 39 additions and 112 deletions

View File

@ -2,10 +2,10 @@ import { useRef, useState } from 'react'
import AvatarEditor from 'react-avatar-editor' import AvatarEditor from 'react-avatar-editor'
import Button from 'react-bootstrap/Button' import Button from 'react-bootstrap/Button'
import BootstrapForm from 'react-bootstrap/Form' import BootstrapForm from 'react-bootstrap/Form'
import Upload from './upload'
import EditImage from '../svgs/image-edit-fill.svg' import EditImage from '../svgs/image-edit-fill.svg'
import Moon from '../svgs/moon-fill.svg' import Moon from '../svgs/moon-fill.svg'
import { useShowModal } from './modal' import { useShowModal } from './modal'
import { ImageUpload } from './image'
export default function Avatar ({ onSuccess }) { export default function Avatar ({ onSuccess }) {
const [uploading, setUploading] = useState() const [uploading, setUploading] = useState()
@ -49,27 +49,41 @@ export default function Avatar ({ onSuccess }) {
} }
return ( return (
<Upload <ImageUpload
as={({ onClick }) => avatar
<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>}
onError={e => { onError={e => {
console.log(e) console.log(e)
setUploading(false) setUploading(false)
}} }}
onSelect={(file, upload) => { 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={({ id }) => {
onSuccess && onSuccess(key) onSuccess?.(id)
setUploading(false) setUploading(false)
}} }}
onStarted={() => { onUpload={() => {
setUploading(true) 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>
) )
} }

View File

@ -259,9 +259,10 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
</Nav.Item> </Nav.Item>
<span className='ms-auto text-muted d-flex align-items-center'> <span className='ms-auto text-muted d-flex align-items-center'>
<ImageUpload <ImageUpload
multiple
ref={imageUploadRef} ref={imageUploadRef}
className='d-flex align-items-center me-1' className='d-flex align-items-center me-1'
onSelect={file => { onUpload={file => {
let text = innerRef.current.value let text = innerRef.current.value
if (text) text += '\n\n' if (text) text += '\n\n'
text += `![Uploading ${file.name}…]()` text += `![Uploading ${file.name}…]()`

View File

@ -1,5 +1,5 @@
import styles from './text.module.css' 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 { IMGPROXY_URL_REGEXP } from '../lib/url'
import { useShowModal } from './modal' import { useShowModal } from './modal'
import { useMe } from './me' import { useMe } from './me'
@ -137,13 +137,14 @@ export default function ZoomableImage ({ src, srcSet, ...props }) {
return <ImageOriginal src={originalUrl} onClick={handleClick} {...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() const toaster = useToast()
ref ??= useRef(null)
const [getSignedPOST] = useMutation( const [getSignedPOST] = useMutation(
gql` gql`
mutation getSignedPOST($type: String!, $size: Int!, $width: Int!, $height: Int!) { mutation getSignedPOST($type: String!, $size: Int!, $width: Int!, $height: Int!, $avatar: Boolean) {
getSignedPOST(type: $type, size: $size, width: $width, height: $height) { getSignedPOST(type: $type, size: $size, width: $width, height: $height, avatar: $avatar) {
url url
fields fields
} }
@ -154,9 +155,10 @@ export const ImageUpload = forwardRef(({ children, className, onSelect, onSucces
img.src = window.URL.createObjectURL(file) img.src = window.URL.createObjectURL(file)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
img.onload = async () => { img.onload = async () => {
onSelect?.(file) onUpload?.(file)
let data let data
const variables = { const variables = {
avatar,
type: file.type, type: file.type,
size: file.size, size: file.size,
width: img.width, width: img.width,
@ -206,7 +208,7 @@ export const ImageUpload = forwardRef(({ children, className, onSelect, onSucces
<input <input
ref={ref} ref={ref}
type='file' type='file'
multiple multiple={multiple}
className='d-none' className='d-none'
accept={UPLOAD_TYPES_ALLOW.join(', ')} accept={UPLOAD_TYPES_ALLOW.join(', ')}
onChange={async (e) => { 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(', ')}`) toaster.danger(`image must be ${UPLOAD_TYPES_ALLOW.map(t => t.replace('image/', '')).join(', ')}`)
continue 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) // TODO find out if this is needed and if so, why (copied from components/upload.js)
e.target.value = null e.target.value = null
} }

View File

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