You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
174 lines
5.8 KiB
174 lines
5.8 KiB
// file-field.tsx
|
|
// Copyright (C) 2020 Dwayne Harris
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
import React, { FC, ChangeEvent, useState } from 'react'
|
|
import { useSelector, useDispatch } from 'react-redux'
|
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
import { faUpload } from '@fortawesome/free-solid-svg-icons'
|
|
import { BlockBlobClient, AnonymousCredential } from '@azure/storage-blob'
|
|
|
|
import { useTheme } from '../../hooks'
|
|
import { setFieldValue } from '../../actions/forms'
|
|
import { showNotification } from '../../actions/notifications'
|
|
import { getFieldValue } from '../../selectors/forms'
|
|
import { apiFetch } from '../../api/fetch'
|
|
import { MEDIA_DEFAULT_MAX_SIZE } from '../../constants'
|
|
import { AppState, AppThunkDispatch, SasResponse, NotificationType } from '../../types'
|
|
|
|
import Progress from '../../components/progress'
|
|
import FieldLabel from '../../components/controls/field-label'
|
|
import AvatarEditor from '../../components/avatar-editor'
|
|
|
|
interface Props {
|
|
name: string
|
|
label: string
|
|
width: number
|
|
height: number
|
|
help?: string
|
|
previewWidth?: number
|
|
maxSize?: number
|
|
}
|
|
|
|
const FileField: FC<Props> = props => {
|
|
const theme = useTheme()
|
|
const value = useSelector<AppState, string>(state => getFieldValue<string>(state, props.name, ''))
|
|
const dispatch = useDispatch<AppThunkDispatch>()
|
|
|
|
const [progress, setProgress] = useState(0)
|
|
const [file, setFile] = useState<File | undefined>()
|
|
const [uploading, setUploading] = useState(false)
|
|
const [uploaded, setUploaded] = useState(false)
|
|
|
|
const {
|
|
name,
|
|
label,
|
|
width,
|
|
height,
|
|
help,
|
|
previewWidth = 128,
|
|
maxSize = MEDIA_DEFAULT_MAX_SIZE,
|
|
} = props
|
|
|
|
const handleChange = async (event: ChangeEvent<HTMLInputElement>) => {
|
|
if (event.target.files && event.target.files[0]) {
|
|
if (event.target.files[0].size > maxSize) {
|
|
const maxSizeString = Math.round(maxSize / 1024 / 1024)
|
|
dispatch(showNotification(NotificationType.Error, `Files must be less than ${maxSizeString} MBs`))
|
|
return
|
|
}
|
|
|
|
setFile(event.target.files[0])
|
|
}
|
|
}
|
|
|
|
const handleUpload = async () => {
|
|
if (!file) return
|
|
setUploading(true)
|
|
|
|
const ext = file.name.substring(file.name.lastIndexOf('.'))
|
|
const { sas, blobUrl, id } = await apiFetch<SasResponse>({ path: '/v1/sas' })
|
|
const filename = `${id}${ext}`
|
|
const url = `${blobUrl}/${filename}`
|
|
|
|
try {
|
|
const blockBlobClient = new BlockBlobClient(`${url}?${sas}`, new AnonymousCredential())
|
|
await blockBlobClient.uploadBrowserData(file, {
|
|
onProgress: p => {
|
|
setProgress((p.loadedBytes / file.size) * 100)
|
|
}
|
|
})
|
|
|
|
await apiFetch({
|
|
path: '/v1/media',
|
|
method: 'post',
|
|
body: {
|
|
name: filename,
|
|
size: file.size,
|
|
type: file.type,
|
|
originalName: file.name,
|
|
}
|
|
})
|
|
|
|
dispatch(setFieldValue(name, url))
|
|
setUploaded(true)
|
|
} catch (err) {
|
|
console.error(err)
|
|
dispatch(showNotification(NotificationType.Error, `Upload error: ${err}`))
|
|
}
|
|
|
|
setUploading(false)
|
|
}
|
|
|
|
const handleDelete = async () => {
|
|
if (uploaded) {
|
|
await apiFetch({
|
|
path: `/v1/media?name=${value}`,
|
|
method: 'delete',
|
|
})
|
|
}
|
|
|
|
dispatch(setFieldValue(name, ''))
|
|
}
|
|
|
|
if (file) {
|
|
return (
|
|
<div className="field">
|
|
<FieldLabel>{label} (<span style={{ color: theme.red }}>Unsaved</span>)</FieldLabel>
|
|
<AvatarEditor
|
|
file={file}
|
|
width={width}
|
|
height={height} />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (uploading) {
|
|
return (
|
|
<div className="field">
|
|
<FieldLabel>{label}</FieldLabel>
|
|
<Progress value={progress} />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="field">
|
|
<FieldLabel>{label}</FieldLabel>
|
|
{value &&
|
|
<div style={{ padding: '1rem 0px' }}>
|
|
<img src={value} style={{ width: previewWidth }} />
|
|
<div style={{ color: theme.secondary, fontSize: '0.8rem' }}>
|
|
<a style={{ color: theme.red }} onClick={() => handleDelete()}>Delete</a>
|
|
</div>
|
|
</div>
|
|
}
|
|
{!value &&
|
|
<div>
|
|
<label className="file-input" style={{ backgroundColor: theme.primary, color: theme.primaryAlternate }}>
|
|
<input type="file" name={name} onChange={handleChange} />
|
|
<span className="button-icon">
|
|
<FontAwesomeIcon icon={faUpload} />
|
|
</span>
|
|
<span>Choose a file...</span>
|
|
</label>
|
|
<p className="help" style={{ color: theme.text }}>{help}</p>
|
|
</div>
|
|
}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default FileField
|