From 9eb2419c9d7e9bbb5e8e468af7a3449f98b6923a Mon Sep 17 00:00:00 2001 From: Jovert Lota Palonpon Date: Tue, 2 Apr 2019 17:01:35 +0800 Subject: [PATCH] wip - Dropzone Component #23 --- resources/js/ui/Dropzone.js | 343 ++++++++++++++++-- resources/js/ui/Loaders/LinearDeterminate.js | 43 ++- resources/js/utils/String.js | 13 + .../views/__backoffice/users/Forms/Avatar.js | 2 +- 4 files changed, 365 insertions(+), 36 deletions(-) diff --git a/resources/js/ui/Dropzone.js b/resources/js/ui/Dropzone.js index bb7aae4..476f539 100644 --- a/resources/js/ui/Dropzone.js +++ b/resources/js/ui/Dropzone.js @@ -1,36 +1,151 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; +import classNames from 'classnames'; import { useDropzone } from 'react-dropzone'; -import { Grid, RootRef, Typography, withStyles } from '@material-ui/core'; -import classNames from 'classnames'; +import { + Button, + colors, + Grid, + Icon, + RootRef, + Tooltip, + Typography, + withStyles, +} from '@material-ui/core'; + +import { + Block as BlockIcon, + CheckCircle as CheckCircleIcon, +} from '@material-ui/icons'; + +import * as StringUtils from '../utils/String'; +import { LinearDeterminate } from './Loaders'; + +const getFileStatusClass = status => { + switch (status) { + case 'uploaded': + return 'fileSuccess'; + break; + + case 'rejected': + return 'fileError'; + break; + + default: + return 'filePrimary'; + break; + } +}; + +let FileIcon = props => { + const { classes, status } = props; + + switch (status) { + case 'uploaded': + return ; + break; + + case 'rejected': + return ; + break; + + default: + return ( + + ); + break; + } +}; + +FileIcon.propTypes = { + classes: PropTypes.object.isRequired, + status: PropTypes.oneOf(['uploading', 'uploaded', 'rejected']).isRequired, +}; + +FileIcon = withStyles(theme => ({ + success: { + color: colors.green[600], + }, + + error: { + color: theme.palette.error.light, + }, + + uploading: { + marginTop: 12, + height: 12, + width: '80%', + marginLeft: 'auto', + marginRight: 'auto', + }, +}))(FileIcon); function Dropzone(props) { - const { classes } = props; + const { classes, acceptedFileTypes, maxFiles, maxFileSize } = props; const [files, setFiles] = useState([]); - const { getRootProps, getInputProps, isDragActive } = useDropzone({ - accept: 'image/*', - maxSize: 1 * 1000 * 1000, - onDrop: acceptedFiles => { - setFiles( - files.concat( - acceptedFiles.map(file => - Object.assign(file, { - url: URL.createObjectURL(file), - }), - ), - ), + + const getFileErrorMessage = file => { + let errors = []; + + if ( + acceptedFileTypes.filter(type => type.indexOf(file.type) < 0) + .length === 0 + ) { + errors.push(`File type is invalid.`); + } + + if (file.size / 1000 / 1000 > maxFileSize) { + errors.push( + `File size is over the limit of ${maxFileSize} megabytes.`, + ); + } + + if (errors.length === 0) { + return 'File not accepted due to unknown error.'; + } + + return errors[0]; + }; + + const { getRootProps, getInputProps, open, isDragActive } = useDropzone({ + accept: acceptedFileTypes.join(','), + maxSize: maxFileSize * 1000 * 1000, + onDrop: (acceptedFiles, rejectedFiles) => { + acceptedFiles = acceptedFiles.map(file => + Object.assign(file, { + url: URL.createObjectURL(file), + status: 'uploading', + }), + ); + + rejectedFiles = rejectedFiles.map(file => + Object.assign(file, { + url: URL.createObjectURL(file), + status: 'rejected', + message: file.hasOwnProperty('message') + ? file.message + : getFileErrorMessage(file), + }), ); + + setFiles(files.concat(acceptedFiles, rejectedFiles)); }, + noClick: true, + noKeyboard: true, }); const { ref, ...rootProps } = getRootProps(); useEffect( () => () => { - // Make sure to revoke the data uris to avoid memory leaks. - files.forEach(file => URL.revokeObjectURL(file.preview)); + files.forEach(file => { + // Make sure to revoke the data uris to avoid memory leaks. + URL.revokeObjectURL(file.preview); + + // Process uploading here. + }); }, [files], ); @@ -50,29 +165,115 @@ function Dropzone(props) { {files.length > 0 ? ( files .map((file, key) => ( - - +
+ {file.type.indexOf('image') > -1 ? ( + + ) : ( +
)} - /> + + + {Math.round( + (file.size / 1000 / 1000) * 100, + ) / 100}{' '} + mb + + + + {StringUtils._limit(file.name, 10)} + + + + + + + +
{ + if (file.status === 'uploading') { + const confirmed = confirm( + 'The file is being uploaded, stop it?', + ); + + if (!confirmed) { + return; + } + } + + setFiles( + files.filter( + (file, i) => i !== key, + ), + ); + }} > Remove File )) .concat([ - +
+ @@ -89,12 +291,27 @@ function Dropzone(props) { , ]) ) : ( - - + + {isDragActive ? `Drag files here` - : `Drag 'n' drop some files here, or click to select files`} + : `Drag 'n' drop some files here`} + + + Or + + + )} @@ -103,7 +320,16 @@ function Dropzone(props) { } Dropzone.propTypes = { + acceptedFileTypes: PropTypes.array, onDrop: PropTypes.func, + maxFiles: PropTypes.number, + maxFileSize: PropTypes.number, +}; + +Dropzone.defaultProps = { + acceptedFileTypes: ['image/*'], + maxFiles: 5, + maxFileSize: 1, }; const styles = theme => ({ @@ -118,20 +344,52 @@ const styles = theme => ({ }, textIcon: { - fontSize: 75, + fontSize: 50, + padding: 20, fontWeight: 'bold', + }, + + textCenter: { textAlign: 'center', }, removeLink: { - textAlign: 'center', '&:hover': { cursor: 'pointer', }, }, + fileWrapper: { + position: 'relative', + '&:hover div': { + display: 'block', + }, + }, + + fileInfo: { + position: 'absolute', + width: '100%', + backgroundColor: 'rgba(255, 255, 255, 0.4)', + color: theme.palette.grey[900], + }, + + fileSize: { + top: 30, + }, + + fileName: { + bottom: 30, + }, + + fileIcon: { + display: 'none', + position: 'absolute', + width: '100%', + fontSize: 35, + top: 45, + }, + file: { - border: `3px solid ${theme.palette.grey[500]}`, borderRadius: '5%', width: 120, height: 120, @@ -139,15 +397,32 @@ const styles = theme => ({ image: { '&:hover': { - filter: 'blur(4px)', + filter: 'blur(2px)', }, }, - addFile: { + fileSuccess: { + border: `2px solid ${colors.green[600]}`, + }, + + fileError: { + border: `2px solid ${theme.palette.error.light}`, + }, + + filePrimary: { + border: `2px solid ${theme.palette.primary.main}`, + }, + + fileAdd: { + border: `2px dashed ${theme.palette.grey[500]}`, '&:hover': { cursor: 'pointer', }, }, + + fileInvalid: { + backgroundColor: theme.palette.grey[300], + }, }); export default withStyles(styles)(Dropzone); diff --git a/resources/js/ui/Loaders/LinearDeterminate.js b/resources/js/ui/Loaders/LinearDeterminate.js index 595f6a6..20b9f94 100644 --- a/resources/js/ui/Loaders/LinearDeterminate.js +++ b/resources/js/ui/Loaders/LinearDeterminate.js @@ -9,13 +9,43 @@ class LinearDeterminate extends Component { }; componentDidMount() { - this.timer = setInterval(this.progress, 500); + this.timer = setInterval( + this.progress, + this.convertSpeed(this.props.speed), + ); } componentWillUnmount() { clearInterval(this.timer); } + convertSpeed = speed => { + switch (speed) { + case 'verySlow': + return 5000; + break; + + case 'slow': + return 2500; + break; + + case 'moderate': + return 1000; + break; + + case 'fast': + return 500; + break; + + case 'veryFast': + return 250; + break; + + default: + break; + } + }; + progress = () => { const { completed } = this.state; @@ -46,8 +76,19 @@ class LinearDeterminate extends Component { } } +LinearDeterminate.defaultProps = { + speed: 'moderate', +}; + LinearDeterminate.propTypes = { classes: PropTypes.object.isRequired, + speed: PropTypes.oneOf([ + 'verySlow', + 'slow', + 'moderate', + 'fast', + 'veryFast', + ]), }; const styles = { diff --git a/resources/js/utils/String.js b/resources/js/utils/String.js index 54f7d17..26f4cd8 100644 --- a/resources/js/utils/String.js +++ b/resources/js/utils/String.js @@ -8,3 +8,16 @@ export function _uppercaseFirst(string) { return string.charAt(0).toUpperCase() + string.slice(1); } + +/** + * Trim the string based on number of characters. + * + * @param {string} string + * @param {number} count + * @param {string} delimiter + * + * @return {string} + */ +export function _limit(string, count, delimiter = '...') { + return string.slice(0, count) + (string.length > count ? delimiter : ''); +} diff --git a/resources/js/views/__backoffice/users/Forms/Avatar.js b/resources/js/views/__backoffice/users/Forms/Avatar.js index 7dfd167..b38dffc 100644 --- a/resources/js/views/__backoffice/users/Forms/Avatar.js +++ b/resources/js/views/__backoffice/users/Forms/Avatar.js @@ -14,7 +14,7 @@ const Avatar = props => { Avatar Upload - +