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 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()
|
||||||
}}
|
}}
|
||||||
onSuccess={async key => {
|
file={file}
|
||||||
onSuccess && onSuccess(key)
|
upload={async (blob) => {
|
||||||
setUploading(false)
|
await upload(blob)
|
||||||
}}
|
resolve(blob)
|
||||||
onStarted={() => {
|
|
||||||
setUploading(true)
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
)))
|
||||||
|
}}
|
||||||
|
onSuccess={({ id }) => {
|
||||||
|
onSuccess?.(id)
|
||||||
|
setUploading(false)
|
||||||
|
}}
|
||||||
|
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>
|
</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}…]()`
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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