diff options
author | Eugene Sokolov <eug-vs@keemail.me> | 2020-08-14 02:50:16 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-08-14 02:50:16 +0300 |
commit | dc0d09f568ca9eeda4978c4750b548ba81688c23 (patch) | |
tree | 08dfa96410207f5621989c3cb0ea930624af1bee | |
parent | 6ace75beae6ab6a466c4d0a9a60ca30aaad0a87c (diff) | |
parent | fca54f49bb3541f726da1becffaa60197835ca68 (diff) | |
download | which-ui-dc0d09f568ca9eeda4978c4750b548ba81688c23.tar.gz |
Merge pull request #80 from which-ecosystem/improved-poll-creation
Upload progress logging
-rw-r--r-- | package-lock.json | 2 | ||||
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | src/containers/Page/Page.tsx | 13 | ||||
-rw-r--r-- | src/containers/PollCreation/ImageInput.tsx (renamed from src/containers/PollCreation/PollCreationImage.tsx) | 36 | ||||
-rw-r--r-- | src/containers/PollCreation/PollCreation.tsx | 77 | ||||
-rw-r--r-- | src/containers/PollCreation/types.ts | 7 | ||||
-rw-r--r-- | src/hooks/useS3Preupload.tsx | 64 |
7 files changed, 145 insertions, 56 deletions
diff --git a/package-lock.json b/package-lock.json index c01707b..d0b962b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "which", - "version": "1.1.0", + "version": "1.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index a18e64d..c2d414f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "which", - "version": "1.1.0", + "version": "1.2.0", "homepage": "https://which-ecosystem.github.io/", "dependencies": { "@material-ui/core": "^4.10.1", diff --git a/src/containers/Page/Page.tsx b/src/containers/Page/Page.tsx index 848ca1d..023d86e 100644 --- a/src/containers/Page/Page.tsx +++ b/src/containers/Page/Page.tsx @@ -1,8 +1,8 @@ -import React, { Suspense } from 'react'; +import React, { Suspense, useEffect } from 'react'; import { makeStyles, useTheme } from '@material-ui/core/styles'; import { useMediaQuery } from '@material-ui/core'; import { SnackbarProvider } from 'notistack'; -import { Switch, Route } from 'react-router-dom'; +import { Switch, Route, useHistory } from 'react-router-dom'; import Loading from '../../components/Loading/Loading'; const Profile = React.lazy(() => import('../Profile/Profile')); @@ -17,7 +17,7 @@ const PollCreation = React.lazy(() => import('../PollCreation/PollCreation')); const useStyles = makeStyles(theme => ({ root: { [theme.breakpoints.down('sm')]: { - margin: theme.spacing(12, 0, 12, 0) + margin: theme.spacing(10, 0, 12, 0) }, [theme.breakpoints.up('md')]: { margin: theme.spacing(15, 5, 8, 5) @@ -29,8 +29,15 @@ const useStyles = makeStyles(theme => ({ const Page: React.FC = () => { const classes = useStyles(); const theme = useTheme(); + const history = useHistory(); const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + useEffect(() => { + return history.listen(() => { + window.scrollTo(0, 0); + }); + }, [history]); + return ( <SnackbarProvider preventDuplicate diff --git a/src/containers/PollCreation/PollCreationImage.tsx b/src/containers/PollCreation/ImageInput.tsx index 1200b11..cc60478 100644 --- a/src/containers/PollCreation/PollCreationImage.tsx +++ b/src/containers/PollCreation/ImageInput.tsx @@ -3,15 +3,17 @@ import { makeStyles } from '@material-ui/core/styles'; import { CardActionArea, CardMedia, - Typography + Typography, + CircularProgress } from '@material-ui/core'; -import ClearIcon from '@material-ui/icons/CancelOutlined'; +import { Check, CancelOutlined } from '@material-ui/icons'; import AttachLink from '../../components/AttachLink/AttachLink'; import FileUpload from '../../components/FileUpload/FileUpload'; interface PropTypes { callback: (file?: File | string) => void; + progress?: number; } const useStyles = makeStyles({ @@ -32,11 +34,25 @@ const useStyles = makeStyles({ display: 'flex', justifyContent: 'center', alignItems: 'center' + }, + darkOverlay: { + backgroundColor: 'rgba(0, 0, 0, 0.45)', + color: 'white', + position: 'absolute', + top: 0, + left: 0, + transitionDuration: '0.5s' + }, + invisible: { + backgroundColor: 'rgba(0, 0, 0, 0)' + }, + icon: { + color: 'white' } }); -const PollCreationImage: React.FC<PropTypes> = ({ callback }) => { +const ImageInput: React.FC<PropTypes> = ({ callback, progress }) => { const classes = useStyles(); const [url, setUrl] = useState<string>(); @@ -59,9 +75,17 @@ const PollCreationImage: React.FC<PropTypes> = ({ callback }) => { ); const Media = ( - <CardActionArea onClick={handleClear} className={classes.root}> + <CardActionArea onClick={handleClear} className={classes.root} disabled={Boolean(progress)}> <CardMedia image={url} className={classes.media}> - <ClearIcon className={classes.clearIcon} /> + <div className={`${classes.media} ${classes.darkOverlay} ${progress === 100 && classes.invisible}`}> + { + progress + ? progress < 100 + ? <CircularProgress variant="static" value={progress} className={classes.icon} /> + : <Check className={classes.icon} fontSize="large" /> + : <CancelOutlined className={classes.icon} fontSize="large" /> + } + </div> </CardMedia> </CardActionArea> ); @@ -69,4 +93,4 @@ const PollCreationImage: React.FC<PropTypes> = ({ callback }) => { return url ? Media : Upload; }; -export default PollCreationImage; +export default ImageInput; diff --git a/src/containers/PollCreation/PollCreation.tsx b/src/containers/PollCreation/PollCreation.tsx index 64ab7fd..107314a 100644 --- a/src/containers/PollCreation/PollCreation.tsx +++ b/src/containers/PollCreation/PollCreation.tsx @@ -1,20 +1,22 @@ -import React, { useState } from 'react'; +import React from 'react'; import { useHistory } from 'react-router-dom'; import { makeStyles } from '@material-ui/core/styles'; import { Button, Card, Divider, - Container + Container, + LinearProgress } from '@material-ui/core'; import { useSnackbar } from 'notistack'; -import axios from 'axios'; -import PollCreationImage from './PollCreationImage'; +import ImageInput from './ImageInput'; import UserStrip from '../../components/UserStrip/UserStrip'; -import { get, post } from '../../requests'; +import { post } from '../../requests'; import { useAuth } from '../../hooks/useAuth'; import { useFeed } from '../../hooks/APIClient'; +import useS3Preupload from '../../hooks/useS3Preupload'; + const useStyles = makeStyles(theme => ({ root: { @@ -26,36 +28,29 @@ const useStyles = makeStyles(theme => ({ } })); + const PollCreation: React.FC = () => { const classes = useStyles(); const history = useHistory(); - const [left, setLeft] = useState<File | string>(); - const [right, setRight] = useState<File | string>(); const { enqueueSnackbar } = useSnackbar(); const { user } = useAuth(); const { mutate: updateFeed } = useFeed(); - - const readyToSubmit = left && right; - - const uploadFile = (file: File): Promise<string> => { - const headers = { 'Content-Type': 'image/png' }; - return get('/files') - .then(response => response.data) - .then(uploadUrl => axios.put(uploadUrl, file, { headers })) - .then(response => { - const { config: { url } } = response; - return url?.slice(0, url?.indexOf('?')) || ''; - }); - }; - - const resolveFile = async (file?: File | string): Promise<string> => { - if (file instanceof File) return uploadFile(file); - return file || ''; - }; + const { + setValue: setLeft, + progress: progressLeft, + resolve: resolveLeft, + isReady: isLeftReady + } = useS3Preupload(); + const { + setValue: setRight, + progress: progressRight, + resolve: resolveRight, + isReady: isRightReady + } = useS3Preupload(); const handleClick = async () => { - if (readyToSubmit) { - const [leftUrl, rightUrl] = await Promise.all([resolveFile(left), resolveFile(right)]); + if (isLeftReady && isRightReady) { + const [leftUrl, rightUrl] = await Promise.all([resolveLeft(), resolveRight()]); const contents = { left: { url: leftUrl }, @@ -79,18 +74,24 @@ const PollCreation: React.FC = () => { {user && <UserStrip user={user} info="" />} <Divider /> <div className={classes.images}> - <PollCreationImage callback={setLeft} /> - <PollCreationImage callback={setRight} /> + <ImageInput callback={setLeft} progress={progressLeft} /> + <ImageInput callback={setRight} progress={progressRight} /> </div> - <Button - color="primary" - disabled={!readyToSubmit} - variant="contained" - onClick={handleClick} - fullWidth - > - Submit - </Button> + { + progressLeft || progressRight + ? <LinearProgress color="primary" /> + : ( + <Button + color="primary" + disabled={!(isLeftReady && isRightReady)} + variant="contained" + onClick={handleClick} + fullWidth + > + Submit + </Button> + ) + } </Card> </Container> ); diff --git a/src/containers/PollCreation/types.ts b/src/containers/PollCreation/types.ts deleted file mode 100644 index 24ace4e..0000000 --- a/src/containers/PollCreation/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface ImageData { - url: string; -} -export interface Contents { - left: ImageData; - right: ImageData; -} diff --git a/src/hooks/useS3Preupload.tsx b/src/hooks/useS3Preupload.tsx new file mode 100644 index 0000000..3c98e9a --- /dev/null +++ b/src/hooks/useS3Preupload.tsx @@ -0,0 +1,64 @@ +import { useState, useCallback, useMemo } from 'react'; +import axios from 'axios'; +import { get } from '../requests'; + + +interface ProgressEvent { + loaded: number; + total: number; +} + +interface Hook { + setValue: (value: File | string | undefined) => void; + isReady: boolean; + resolve: () => Promise<string>; + progress: number; +} + +export default (): Hook => { + const [url, setUrl] = useState<string>(); + const [file, setFile] = useState<File>(); + const [progress, setProgress] = useState<number>(0); + + const isReady = useMemo(() => Boolean(file || url), [file, url]); + + const setValue: Hook['setValue'] = useCallback(value => { + if (value instanceof File) { + setFile(value); + setUrl(undefined); + } else { + setUrl(value); + setFile(undefined); + } + }, [setUrl, setFile]); + + const handleUploadProgress = useCallback((progressEvent: ProgressEvent): void => { + setProgress(Math.round((progressEvent.loaded * 100) / progressEvent.total)); + }, [setProgress]); + + const resolve = useCallback(async (): Promise<string> => { + if (file) { + const config = { + headers: { 'Content-Type': 'image/png' }, + onUploadProgress: handleUploadProgress + }; + + return get('/files') + .then(response => response.data) + .then(uploadUrl => axios.put(uploadUrl, file, config)) + .then(response => { + const uri = response.config.url; + return uri ? uri.slice(0, uri.indexOf('?')) : ''; + }); + } + setProgress(100); + return url || ''; + }, [file, handleUploadProgress, url]); + + return { + setValue, + isReady, + resolve, + progress + }; +}; |