diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/components/FileUpload/FileUpload.tsx | 8 | ||||
-rw-r--r-- | src/containers/PollCreation/ImageInput.tsx | 10 | ||||
-rw-r--r-- | src/containers/PollCreation/PollCreation.tsx | 2 | ||||
-rw-r--r-- | src/containers/PollCreation/useS3Preupload.ts | 40 | ||||
-rw-r--r-- | src/containers/Profile/ProfileInfo.tsx | 23 | ||||
-rw-r--r-- | src/hooks/useS3Preupload.tsx | 73 | ||||
-rw-r--r-- | src/utils/files.ts | 29 | ||||
-rw-r--r-- | src/utils/getLocalFileUrl.ts | 15 | ||||
-rw-r--r-- | src/utils/uploadFileToS3.ts | 57 |
9 files changed, 135 insertions, 122 deletions
diff --git a/src/components/FileUpload/FileUpload.tsx b/src/components/FileUpload/FileUpload.tsx index d455f62..961fa9a 100644 --- a/src/components/FileUpload/FileUpload.tsx +++ b/src/components/FileUpload/FileUpload.tsx @@ -1,10 +1,9 @@ import React, { useRef } from 'react'; import Button from '@material-ui/core/Button'; import CloudUpload from '@material-ui/icons/CloudUpload'; -import { getLocalFileUrl } from '../../utils/files'; interface PropTypes { - callback: (fileUrl: string, file: File) => void; + callback: (file: File) => void; } @@ -13,10 +12,7 @@ const FileUpload: React.FC<PropTypes> = ({ callback, children }) => { const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { const files = event.target?.files; - if (files?.length) { - const file = files[0]; - getLocalFileUrl(file).then(url => callback(url, file)); - }; + if (files?.length) callback(files[0]); }; const handleClick = () => { diff --git a/src/containers/PollCreation/ImageInput.tsx b/src/containers/PollCreation/ImageInput.tsx index 475d527..e807865 100644 --- a/src/containers/PollCreation/ImageInput.tsx +++ b/src/containers/PollCreation/ImageInput.tsx @@ -10,6 +10,7 @@ import { Check, CancelOutlined } from '@material-ui/icons'; import AttachLink from '../../components/AttachLink/AttachLink'; import FileUpload from '../../components/FileUpload/FileUpload'; import BackgroundImage from '../../components/Image/BackgroundImage'; +import getLocalFileUrl from '../../utils/getLocalFileUrl'; interface PropTypes { callback: (file?: File | string) => void; @@ -59,9 +60,12 @@ const ImageInput: React.FC<PropTypes> = ({ callback, progress }) => { callback(undefined); }; - const childrenCallback = (fileUrl: string, file?: File) => { - setUrl(fileUrl); - callback(file || fileUrl); + const childrenCallback = (value: File | string) => { + if (value instanceof File) { + getLocalFileUrl(value).then(localUrl => setUrl(localUrl)); + } else setUrl(value); + + callback(value); }; const Upload = ( diff --git a/src/containers/PollCreation/PollCreation.tsx b/src/containers/PollCreation/PollCreation.tsx index 03ab905..87bdcf7 100644 --- a/src/containers/PollCreation/PollCreation.tsx +++ b/src/containers/PollCreation/PollCreation.tsx @@ -12,12 +12,12 @@ import { } from '@material-ui/core'; import { useSnackbar } from 'notistack'; +import useS3Preupload from './useS3Preupload'; import ImageInput from './ImageInput'; import UserStrip from '../../components/UserStrip/UserStrip'; import { post } from '../../requests'; import { useAuth } from '../../hooks/useAuth'; import { useFeed } from '../../hooks/APIClient'; -import useS3Preupload from '../../hooks/useS3Preupload'; const useStyles = makeStyles(theme => ({ diff --git a/src/containers/PollCreation/useS3Preupload.ts b/src/containers/PollCreation/useS3Preupload.ts new file mode 100644 index 0000000..ef3a408 --- /dev/null +++ b/src/containers/PollCreation/useS3Preupload.ts @@ -0,0 +1,40 @@ +import { useState, useCallback, useMemo } from 'react'; +import uploadFileToS3 from '../../utils/uploadFileToS3'; + + +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 resolve = useCallback(async (quality?: number): Promise<string> => { + if (file) return uploadFileToS3(file, quality, setProgress); + return url || ''; + }, [file, url]); + + return { + setValue, + isReady, + resolve, + progress + }; +}; diff --git a/src/containers/Profile/ProfileInfo.tsx b/src/containers/Profile/ProfileInfo.tsx index c9831f3..fbd6272 100644 --- a/src/containers/Profile/ProfileInfo.tsx +++ b/src/containers/Profile/ProfileInfo.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import { Badge, Typography } from '@material-ui/core/'; import { makeStyles } from '@material-ui/core/styles'; import { User } from 'which-types'; @@ -6,10 +6,11 @@ import CameraAltIcon from '@material-ui/icons/CameraAlt'; import VerifiedIcon from '@material-ui/icons/CheckCircleOutline'; import Skeleton from '@material-ui/lab/Skeleton'; import Highlight from './Highlight'; -import AttachLink from '../../components/AttachLink/AttachLink'; +import FileUpload from '../../components/FileUpload/FileUpload'; import Avatar from '../../components/Avatar/Avatar'; import { patch } from '../../requests'; import { useAuth } from '../../hooks/useAuth'; +import uploadFileToS3 from '../../utils/uploadFileToS3'; interface PropTypes { @@ -88,14 +89,16 @@ const ProfileInfo: React.FC<PropTypes> = ({ }) => { const classes = useStyles(); const { user } = useAuth(); + const dateSince = new Date(userInfo?.createdAt || '').toLocaleDateString(); - const patchAvatar = (url: string) => { - const id = user?._id; - patch(`/users/${id}`, { avatarUrl: url }).then(res => { - setUserInfo(res.data); - }); - }; + const handleUpdateAvatar = useCallback(async (file: File) => { + if (user) { + uploadFileToS3(file, 1) + .then(avatarUrl => patch(`/users/${user._id}`, { avatarUrl })) + .then(response => setUserInfo(response.data)); + } + }, [user, setUserInfo]); return ( <div className={classes.root}> @@ -104,7 +107,7 @@ const ProfileInfo: React.FC<PropTypes> = ({ ? <Skeleton animation="wave" variant="circle" width={150} height={150} className={classes.avatar} /> : userInfo?._id === user?._id ? ( - <AttachLink callback={patchAvatar}> + <FileUpload callback={handleUpdateAvatar}> <div className={classes.avatarContainer}> <Badge overlap="circle" @@ -121,7 +124,7 @@ const ProfileInfo: React.FC<PropTypes> = ({ <Avatar className={classes.avatar} user={userInfo} /> </Badge> </div> - </AttachLink> + </FileUpload> ) : <Avatar className={classes.avatar} user={userInfo} /> } diff --git a/src/hooks/useS3Preupload.tsx b/src/hooks/useS3Preupload.tsx deleted file mode 100644 index 9516459..0000000 --- a/src/hooks/useS3Preupload.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { useState, useCallback, useMemo } from 'react'; -import axios from 'axios'; -import Bluebird from 'bluebird'; -import { get } from '../requests'; -import { compressFile } from '../utils/files'; - -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 => { - // Only allow upload progress reach 95%, and set 100% when request is resolved - setProgress(Math.round((progressEvent.loaded * 95) / progressEvent.total)); - }, [setProgress]); - - const resolve = useCallback(async (quality?: number): Promise<string> => { - if (file) { - const config = { - headers: { 'Content-Type': 'image/png' }, - onUploadProgress: handleUploadProgress - }; - - setProgress(0.01); - - // Add querystring to avoid caching request in some browsers, see - // https://stackoverflow.com/questions/59339561/safari-skipping-xmlhttprequests - return Bluebird.all([get(`/files?key=${file.name}`), compressFile(file, quality)]) - .then(([response, compressedFile]) => { - const uploadUrl = response.data; - return axios.put(uploadUrl, compressedFile, config); - }) - .then(response => { - setProgress(100); - const uri = response.config.url; - return uri ? uri.slice(0, uri.indexOf('?')) : ''; - }); - } - setProgress(100); - return url || ''; - }, [file, handleUploadProgress, url]); - - return { - setValue, - isReady, - resolve, - progress - }; -}; diff --git a/src/utils/files.ts b/src/utils/files.ts deleted file mode 100644 index 5d16fdb..0000000 --- a/src/utils/files.ts +++ /dev/null @@ -1,29 +0,0 @@ -import Compressor from 'compressorjs'; - - -export const getLocalFileUrl = (file: File): Promise<string> => { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - - reader.addEventListener('load', () => { - if (typeof reader.result === 'string') resolve(reader.result); - }, false); - - reader.addEventListener('error', () => { - reject(new Error('Error reading the file')) - }, false ); - - if (file) reader.readAsDataURL(file); - }); -}; - -export const compressFile = (file: File, quality = 0.6): Promise<Blob> => { - return new Promise((resolve, reject) => { - return new Compressor(file, { - success: result => resolve(result), - error: err => reject(err), - quality - }); - }); -}; - diff --git a/src/utils/getLocalFileUrl.ts b/src/utils/getLocalFileUrl.ts new file mode 100644 index 0000000..1469804 --- /dev/null +++ b/src/utils/getLocalFileUrl.ts @@ -0,0 +1,15 @@ +export default (file: File): Promise<string> => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + + reader.addEventListener('load', () => { + if (typeof reader.result === 'string') resolve(reader.result); + }, false); + + reader.addEventListener('error', () => { + reject(new Error('Error reading the file')); + }, false); + + if (file) reader.readAsDataURL(file); + }); +}; diff --git a/src/utils/uploadFileToS3.ts b/src/utils/uploadFileToS3.ts new file mode 100644 index 0000000..de1b1aa --- /dev/null +++ b/src/utils/uploadFileToS3.ts @@ -0,0 +1,57 @@ +import Compressor from 'compressorjs'; +import axios from 'axios'; +import Bluebird from 'bluebird'; +import { get } from '../requests'; + +interface ProgressEvent { + loaded: number; + total: number; +} + +const compressFile = (file: File, quality = 0.6): Promise<File | Blob> => { + return new Promise((resolve, reject) => { + if (quality === 1) resolve(file); + else return new Compressor(file, { + success: result => resolve(result), + error: err => reject(err), + quality + }); + }); +}; + +export default ( + file: File, + quality?: number, + setProgress?: (progress: number) => void +): Promise<string> => { + + const onUploadProgress = (progressEvent: ProgressEvent): void => { + if (setProgress) { + // Only allow upload progress reach 95%, and set 100% when request is resolved + const progress = Math.round((progressEvent.loaded * 95) / progressEvent.total); + setProgress(progress); + } + }; + + const config = { + headers: { 'Content-Type': 'image/png' }, + onUploadProgress + }; + + // Indicate start + if (setProgress) setProgress(0.01); + + // Add querystring to avoid caching request in some browsers, see + // https://stackoverflow.com/questions/59339561/safari-skipping-xmlhttprequests + return Bluebird.all([get(`/files?key=${file.name}`), compressFile(file, quality)]) + .then(([response, compressedFile]) => { + const uploadUrl = response.data; + return axios.put(uploadUrl, compressedFile, config); + }) + .then(response => { + if (setProgress) setProgress(100); + const uri = response.config.url; + return uri ? uri.slice(0, uri.indexOf('?')) : ''; + }); +}; + |