refactor: Unify <ImageUpload> and <Upload> component
This commit is contained in:
parent
1708c77458
commit
7043b4f1d1
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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}…]()`
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()} />
|
||||
</>
|
||||
)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user