|
|
// 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
|