aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/containers/Page/Page.tsx13
-rw-r--r--src/containers/PollCreation/ImageInput.tsx (renamed from src/containers/PollCreation/PollCreationImage.tsx)36
-rw-r--r--src/containers/PollCreation/PollCreation.tsx77
-rw-r--r--src/containers/PollCreation/types.ts7
-rw-r--r--src/hooks/useS3Preupload.tsx64
5 files changed, 143 insertions, 54 deletions
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
+ };
+};