From d9a4d96f25f7cb6b7c35517adb2c02ec66f2e42e Mon Sep 17 00:00:00 2001 From: gurkiran_singh Date: Tue, 8 Jun 2021 03:02:52 -0400 Subject: [PATCH 1/9] Added background and profile image in Profile Photo page Signed-off-by: gurkiran_singh --- .../components/EditProfile/EditProfile.tsx | 1 - .../components/ProfilePhoto/ProfilePhoto.tsx | 75 +++++++++++++---- .../src/components/ProfilePhoto/useStyles.ts | 11 +++ client/src/pages/Settings/Settings.tsx | 81 +------------------ 4 files changed, 71 insertions(+), 97 deletions(-) diff --git a/client/src/components/EditProfile/EditProfile.tsx b/client/src/components/EditProfile/EditProfile.tsx index b157a67..83b56dd 100644 --- a/client/src/components/EditProfile/EditProfile.tsx +++ b/client/src/components/EditProfile/EditProfile.tsx @@ -1,4 +1,3 @@ -import { useState } from 'react'; import TextField from '@material-ui/core/TextField'; import InputLabel from '@material-ui/core/InputLabel'; import Grid from '@material-ui/core/Grid'; diff --git a/client/src/components/ProfilePhoto/ProfilePhoto.tsx b/client/src/components/ProfilePhoto/ProfilePhoto.tsx index 7a870a5..8046928 100644 --- a/client/src/components/ProfilePhoto/ProfilePhoto.tsx +++ b/client/src/components/ProfilePhoto/ProfilePhoto.tsx @@ -7,45 +7,86 @@ import CardMedia from '@material-ui/core/CardMedia'; import Typography from '@material-ui/core/Typography'; import DeleteOutlineIcon from '@material-ui/icons/DeleteOutline'; import { useAuth } from '../../context/useAuthContext'; +import { useSnackBar } from '../../context/useSnackbarContext'; import useStyles from './useStyles'; +interface ImagesState { + background: string; + profile: string; + additionalOne: string; + additionalTwo: string; +} + const ProfilePhoto = (): JSX.Element => { - const [imgSrc, setImgSrc] = useState(''); + const [images, setImages] = useState({ + background: '', + profile: '', + additionalOne: '', + additionalTwo: '', + }); const classes = useStyles(); const { loggedInUser } = useAuth(); + const { updateSnackBarMessage } = useSnackBar(); - const handleImageUpload = (event: React.ChangeEvent) => { - if (event.target.files?.length) { - const src = URL.createObjectURL(event.target.files[0]); - setImgSrc(src); + const handleImageUpload = (event: React.ChangeEvent): void => { + if (event.target.files?.length === 2) { + const background = URL.createObjectURL(event.target.files[0]); + const profile = URL.createObjectURL(event.target.files[1]); + // const additionalOne = URL.createObjectURL(event.target.files[2]); + // const additionalTwo = URL.createObjectURL(event.target.files[3]); + setImages({ ...images, profile, background }); + } else { + updateSnackBarMessage('Make sure you upload 2 pictures. 1. background and 2. Profile'); } }; + const handleDeleteIcon = (): void => { + setImages({ + background: '', + profile: '', + additionalOne: '', + additionalTwo: '', + }); + }; + return ( <> + - - Be sure to use a photo that - - clearly shows your face - - + + Be sure to use a photo that + + clearly shows your face + + - - + + diff --git a/client/src/components/ProfilePhoto/useStyles.ts b/client/src/components/ProfilePhoto/useStyles.ts index 97c5c5c..1a6e369 100644 --- a/client/src/components/ProfilePhoto/useStyles.ts +++ b/client/src/components/ProfilePhoto/useStyles.ts @@ -16,6 +16,7 @@ const useStyles = makeStyles((theme) => ({ alignItems: 'center', }, mediaContainer: { + position: 'relative', display: 'flex', flexDirection: 'column', alignItems: 'center', @@ -24,10 +25,20 @@ const useStyles = makeStyles((theme) => ({ marginTop: '1rem', }, }, + backgroundMedia: { + maxHeight: '300px', + }, media: { + position: 'absolute', width: '25%', + bottom: '-23%', + border: '4px solid white', borderRadius: '6rem', }, + profileMediaInfo: { + textAlign: 'center', + marginTop: '5rem', + }, upload: { margin: theme.spacing(2, 2, 2), padding: 10, diff --git a/client/src/pages/Settings/Settings.tsx b/client/src/pages/Settings/Settings.tsx index 22aeb13..ee30aa4 100644 --- a/client/src/pages/Settings/Settings.tsx +++ b/client/src/pages/Settings/Settings.tsx @@ -1,9 +1,8 @@ -import { useState, ChangeEvent } from 'react'; +import { useState } from 'react'; import CssBaseline from '@material-ui/core/CssBaseline'; import Container from '@material-ui/core/Container'; import Card from '@material-ui/core/Card'; import Grid from '@material-ui/core/Grid'; -import Typography from '@material-ui/core/Typography'; import Tabs from '@material-ui/core/Tabs'; import Tab from '@material-ui/core/Tab'; @@ -42,9 +41,8 @@ const TabPanel = (props: TabPanelProps) => { const Settings = (): JSX.Element => { const classes = useStyles(); - const [currentTab, setCurrentTab] = useState(''); const [currentTabIndex, setCurrentTabIndex] = useState(0); - const handleTabIndexChange = (event: any, newValue: number) => { + const handleTabIndexChange = (event: any, newValue: number): void => { setCurrentTabIndex(newValue); }; return ( @@ -67,13 +65,6 @@ const Settings = (): JSX.Element => { - {/* {currentTab === 'profilePhoto' ? ( - - ) : currentTab === 'payment' ? ( - - ) : ( - - )} */} @@ -91,71 +82,3 @@ const Settings = (): JSX.Element => { }; export default Settings; - -// import { makeStyles } from '@material-ui/core/styles'; - -// function TabPanel(props: any) { -// const { children, value, index, ...other } = props; - -// return ( -// -// ); -// } - -// const a11yProps = (index: number) => { -// return { -// id: `vertical-tab-${index}`, -// 'aria-controls': `vertical-tabpanel-${index}`, -// }; -// }; - -// const useStyles = makeStyles((theme) => ({ -// root: { -// // flexGrow: 1, -// // backgroundColor: theme.palette.background.paper, -// // display: 'flex', -// // height: 224, -// }, -// })); - -// export default function VerticalTabs() { -// const classes = useStyles(); -// const [value, setValue] = useState(0); - -// const handleChange = (event: any, newValue: any) => { -// setValue(newValue); -// }; - -// return ( -//
-// -// -// -// -// -// -// Item One -// -// -// Item Two -// -// -// Item Three -// -//
-// ); -// } From 617e17b10efa84b8ab5a84e11c442261c937da24 Mon Sep 17 00:00:00 2001 From: gurkiran_singh Date: Tue, 8 Jun 2021 22:15:22 -0400 Subject: [PATCH 2/9] Added frontend upload integration to backend Signed-off-by: gurkiran_singh --- .../components/ProfilePhoto/ProfilePhoto.tsx | 88 ++++++++++++++----- .../src/components/ProfilePhoto/useStyles.ts | 10 +++ client/src/helpers/APICalls/uploadImages.ts | 20 +++++ client/src/interface/FetchOptions.ts | 5 +- client/src/pages/Dashboard/Dashboard.tsx | 3 +- server/controllers/s3.js | 59 ++++++++----- server/routes/s3.js | 10 ++- 7 files changed, 144 insertions(+), 51 deletions(-) create mode 100644 client/src/helpers/APICalls/uploadImages.ts diff --git a/client/src/components/ProfilePhoto/ProfilePhoto.tsx b/client/src/components/ProfilePhoto/ProfilePhoto.tsx index 8046928..aa252b6 100644 --- a/client/src/components/ProfilePhoto/ProfilePhoto.tsx +++ b/client/src/components/ProfilePhoto/ProfilePhoto.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import { useState, ChangeEvent } from 'react'; import Box from '@material-ui/core/Box'; import Button from '@material-ui/core/Button'; import CardHeader from '@material-ui/core/CardHeader'; @@ -10,34 +10,39 @@ import { useAuth } from '../../context/useAuthContext'; import { useSnackBar } from '../../context/useSnackbarContext'; import useStyles from './useStyles'; +import uploadImagesAPI from '../../helpers/APICalls/uploadImages'; interface ImagesState { background: string; profile: string; - additionalOne: string; - additionalTwo: string; +} + +interface UploadImagesState { + background: string | File; + profile: string | File; } const ProfilePhoto = (): JSX.Element => { const [images, setImages] = useState({ background: '', profile: '', - additionalOne: '', - additionalTwo: '', + }); + const [uploadImages, setUploadImages] = useState({ + background: '', + profile: '', }); const classes = useStyles(); const { loggedInUser } = useAuth(); const { updateSnackBarMessage } = useSnackBar(); - - const handleImageUpload = (event: React.ChangeEvent): void => { - if (event.target.files?.length === 2) { + const handleImageUpload = (event: ChangeEvent): void => { + if (images.background === '' && event.target.files?.length) { const background = URL.createObjectURL(event.target.files[0]); - const profile = URL.createObjectURL(event.target.files[1]); - // const additionalOne = URL.createObjectURL(event.target.files[2]); - // const additionalTwo = URL.createObjectURL(event.target.files[3]); - setImages({ ...images, profile, background }); - } else { - updateSnackBarMessage('Make sure you upload 2 pictures. 1. background and 2. Profile'); + setImages((prevState) => ({ ...prevState, background })); + setUploadImages((prevState) => ({ ...prevState, background: event.target.files![0] })); + } else if (images.profile === '' && event.target.files?.length) { + const profile = URL.createObjectURL(event.target.files[0]); + setImages((prevState) => ({ ...prevState, profile })); + setUploadImages((prevState) => ({ ...prevState, profile: event.target.files![0] })); } }; @@ -45,8 +50,33 @@ const ProfilePhoto = (): JSX.Element => { setImages({ background: '', profile: '', - additionalOne: '', - additionalTwo: '', + }); + setUploadImages({ + background: '', + profile: '', + }); + }; + + const handleImageUploads = (): void => { + if (!uploadImages.background && !uploadImages.profile) return; + const formData = new FormData(); + formData.set('background', uploadImages.background); + formData.set('profile', uploadImages.profile); + uploadImagesAPI(formData).then((data: any): void => { + console.log(data); + if (data.error) { + if (data.error.message) { + updateSnackBarMessage(data.error.message); + } else { + updateSnackBarMessage(data.error); + } + } else if (data.success) { + updateSnackBarMessage(data.success); + } else { + // should not get here from backend but this catch is for an unknown issue + console.error({ data }); + updateSnackBarMessage('An unexpected error occurred. Please try again'); + } }); }; @@ -62,8 +92,8 @@ const ProfilePhoto = (): JSX.Element => { ? images.background : `https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRmFJpMlEV-41ZFT8U7iUsMJzaXVIL_hAtT9A&usqp=CAU` } - title="User Profile Picture" - alt="Your Profile" + title="Background Picture" + alt="Your Background" /> { + + + ); diff --git a/client/src/components/ProfilePhoto/useStyles.ts b/client/src/components/ProfilePhoto/useStyles.ts index 1a6e369..bb1fcca 100644 --- a/client/src/components/ProfilePhoto/useStyles.ts +++ b/client/src/components/ProfilePhoto/useStyles.ts @@ -48,6 +48,16 @@ const useStyles = makeStyles((theme) => ({ fontWeight: 'bold', fontSize: '0.8rem', }, + submit: { + margin: theme.spacing(3, 2, 2), + padding: 10, + width: 160, + height: 56, + borderRadius: theme.shape.borderRadius, + marginTop: 49, + textTransform: 'uppercase', + fontWeight: 'bold', + }, })); export default useStyles; diff --git a/client/src/helpers/APICalls/uploadImages.ts b/client/src/helpers/APICalls/uploadImages.ts new file mode 100644 index 0000000..c4529c4 --- /dev/null +++ b/client/src/helpers/APICalls/uploadImages.ts @@ -0,0 +1,20 @@ +import { AuthApiData } from '../../interface/AuthApiData'; +import { FetchOptions } from '../../interface/FetchOptions'; + +const uploadImagesAPI = async (data: FormData): Promise => { + const fetchOptions: FetchOptions = { + method: 'POST', + headers: { + Accept: 'application/json', + }, + body: data, + credentials: 'include', + }; + return await fetch(`/upload/uploadimage`, fetchOptions) + .then((res) => res.json()) + .catch(() => ({ + error: { message: 'Unable to connect to server. Please try again' }, + })); +}; + +export default uploadImagesAPI; diff --git a/client/src/interface/FetchOptions.ts b/client/src/interface/FetchOptions.ts index 076a4c8..1cc8858 100755 --- a/client/src/interface/FetchOptions.ts +++ b/client/src/interface/FetchOptions.ts @@ -1,8 +1,9 @@ export interface FetchOptions { method: string; headers?: { - "Content-Type": string; + Accept?: string; + "Content-Type"?: string; }; - body?: string; + body?: string | FormData; credentials: RequestCredentials; } diff --git a/client/src/pages/Dashboard/Dashboard.tsx b/client/src/pages/Dashboard/Dashboard.tsx index 879abf3..1125140 100755 --- a/client/src/pages/Dashboard/Dashboard.tsx +++ b/client/src/pages/Dashboard/Dashboard.tsx @@ -4,7 +4,7 @@ import CircularProgress from '@material-ui/core/CircularProgress'; import useStyles from './useStyles'; import { useAuth } from '../../context/useAuthContext'; import { useSocket } from '../../context/useSocketContext'; -import { useHistory } from 'react-router-dom'; +import { useHistory, Link } from 'react-router-dom'; import ChatSideBanner from '../../components/ChatSideBanner/ChatSideBanner'; import { useEffect } from 'react'; @@ -29,6 +29,7 @@ export default function Dashboard(): JSX.Element { return ( + Settings diff --git a/server/controllers/s3.js b/server/controllers/s3.js index d31e04d..ffebd2a 100644 --- a/server/controllers/s3.js +++ b/server/controllers/s3.js @@ -1,37 +1,48 @@ const AWS = require("aws-sdk"); exports.uploadImage = (req, res) => { - if(req.file.size > 1 * 1024 * 1024) { - return res.status(400).json({ - error: "Please, only upload only image upto 1Mb of size" - }); - }; AWS.config.update({ accessKeyId: process.env.IAM_ACCESS_KEY, secretAccessKey: process.env.IAM_SECRET_ACCESS_KEY, region: process.env.REGION }); const s3 = new AWS.S3(); - const params = { - Bucket: process.env.BUCKET_NAME, - Body: req.file.buffer, - ContentType: req.file.mimetype, - Key: `photo/${req.file.originalname}` - }; - s3.upload(params, (error, data) => { - if (error) { - console.log(error); - return res.status(error.status).json({ - error: `${error.message}` - }); - } + let locationUrls = []; - if(data) { - const locationUrl = data.Location; - // store locationUrl to database - } + const fieldNames = ['background', 'profile']; + + for (let index = 0; index < fieldNames.length; index++) { + const fieldName = fieldNames[index]; + const file = req.files[fieldName]; + const params = { + Bucket: process.env.BUCKET_NAME, + Body: file[0].buffer, + ContentType: file[0].mimetype, + Key: `${req.user.id}/${file[0].fieldname}`, + Expires: 604800 + }; + s3.upload(params, (error, data) => { + if (error) { + console.log(error); + return res.status(error.status).json({ + error: `${error.message}` + }); + } + + if(data) { + // store locationUrl in Database; + const locationUrl = data.Location; + locationUrls.push({ locationUrl, key: data.key }) + if (locationUrls.length === fieldNames.length) { + return res.status(201).json({ + locationUrls, + success: "File are saved successfully." + }) + + } + } + }) + } - return res.status(200).json({ data }); - }) }; diff --git a/server/routes/s3.js b/server/routes/s3.js index 1404dce..b5b0b84 100644 --- a/server/routes/s3.js +++ b/server/routes/s3.js @@ -2,16 +2,20 @@ const multer = require("multer"); const express = require("express"); const router = express.Router(); +const protect = require("../middleware/auth"); const { uploadImage } = require("../controllers/s3") const upload = multer({ limits: { - fieldSize: 1 * 1024 * 1024 + fileSize: 1.5 * 1024 * 1024 // 1.5 MB limit } }) - .array('photos', 5); + .fields([ + { name: "background", maxCount: 1 }, + { name: "profile", maxCount: 1 }, + ]) -router.post("/uploadimage", upload, uploadImage); +router.post("/uploadimage", protect, upload, uploadImage); module.exports = router; From 8953d3635c89eefe4141e7ce7b89f130c33fa203 Mon Sep 17 00:00:00 2001 From: gurkiran_singh Date: Wed, 9 Jun 2021 13:19:48 -0400 Subject: [PATCH 3/9] remove the expires from the params in s3 controller Signed-off-by: gurkiran_singh --- server/controllers/s3.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server/controllers/s3.js b/server/controllers/s3.js index ffebd2a..f7ed0a9 100644 --- a/server/controllers/s3.js +++ b/server/controllers/s3.js @@ -1,11 +1,12 @@ const AWS = require("aws-sdk"); +AWS.config.update({ + accessKeyId: process.env.ACCESS_KEY, + secretAccessKey: process.env.SECRET_ACCESS_KEY, + region: process.env.REGION +}); + exports.uploadImage = (req, res) => { - AWS.config.update({ - accessKeyId: process.env.IAM_ACCESS_KEY, - secretAccessKey: process.env.IAM_SECRET_ACCESS_KEY, - region: process.env.REGION - }); const s3 = new AWS.S3(); let locationUrls = []; @@ -20,7 +21,6 @@ exports.uploadImage = (req, res) => { Body: file[0].buffer, ContentType: file[0].mimetype, Key: `${req.user.id}/${file[0].fieldname}`, - Expires: 604800 }; s3.upload(params, (error, data) => { if (error) { From cd1ed5802c88531de446d0acc8e40700a7fd876d Mon Sep 17 00:00:00 2001 From: gurkiran_singh Date: Thu, 10 Jun 2021 15:09:42 -0400 Subject: [PATCH 4/9] Added the interface and userContext for images Signed-off-by: gurkiran_singh --- client/src/App.tsx | 5 ++- .../components/ProfilePhoto/ProfilePhoto.tsx | 44 +++++++----------- client/src/context/useUserContext.tsx | 45 +++++++++++++++++++ client/src/helpers/APICalls/uploadImages.ts | 4 +- client/src/interface/S3Upload.ts | 5 +++ client/src/pages/Dashboard/Dashboard.tsx | 3 +- client/src/pages/Settings/Settings.tsx | 1 + 7 files changed, 74 insertions(+), 33 deletions(-) create mode 100644 client/src/context/useUserContext.tsx create mode 100644 client/src/interface/S3Upload.ts diff --git a/client/src/App.tsx b/client/src/App.tsx index 4da1fc5..8106b3f 100755 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -5,6 +5,7 @@ import { BrowserRouter } from 'react-router-dom'; import { AuthProvider } from './context/useAuthContext'; import { SocketProvider } from './context/useSocketContext'; import { SnackBarProvider } from './context/useSnackbarContext'; +import { UserProvider } from './context/useUserContext'; import Routes from './Routes'; import './App.css'; @@ -16,7 +17,9 @@ function App(): JSX.Element { - + + + diff --git a/client/src/components/ProfilePhoto/ProfilePhoto.tsx b/client/src/components/ProfilePhoto/ProfilePhoto.tsx index aa252b6..62cec35 100644 --- a/client/src/components/ProfilePhoto/ProfilePhoto.tsx +++ b/client/src/components/ProfilePhoto/ProfilePhoto.tsx @@ -8,49 +8,41 @@ import Typography from '@material-ui/core/Typography'; import DeleteOutlineIcon from '@material-ui/icons/DeleteOutline'; import { useAuth } from '../../context/useAuthContext'; import { useSnackBar } from '../../context/useSnackbarContext'; +import { useUser } from '../../context/useUserContext'; import useStyles from './useStyles'; import uploadImagesAPI from '../../helpers/APICalls/uploadImages'; -interface ImagesState { - background: string; - profile: string; -} - interface UploadImagesState { background: string | File; profile: string | File; } const ProfilePhoto = (): JSX.Element => { - const [images, setImages] = useState({ - background: '', - profile: '', - }); const [uploadImages, setUploadImages] = useState({ background: '', profile: '', }); + const { userState, dispatchUserContext } = useUser(); const classes = useStyles(); const { loggedInUser } = useAuth(); const { updateSnackBarMessage } = useSnackBar(); const handleImageUpload = (event: ChangeEvent): void => { - if (images.background === '' && event.target.files?.length) { + if (userState.background === '' && event.target.files?.length) { const background = URL.createObjectURL(event.target.files[0]); - setImages((prevState) => ({ ...prevState, background })); + dispatchUserContext({ type: 'UPLOAD_BACKGROUND', background }); + // eslint-disable-next-line setUploadImages((prevState) => ({ ...prevState, background: event.target.files![0] })); - } else if (images.profile === '' && event.target.files?.length) { + } else if (userState.profile === '' && event.target.files?.length) { const profile = URL.createObjectURL(event.target.files[0]); - setImages((prevState) => ({ ...prevState, profile })); + dispatchUserContext({ type: 'UPLOAD_PROFILE', profile }); + // eslint-disable-next-line setUploadImages((prevState) => ({ ...prevState, profile: event.target.files![0] })); } }; const handleDeleteIcon = (): void => { - setImages({ - background: '', - profile: '', - }); + dispatchUserContext({ type: 'EMPTY_IMAGES' }); setUploadImages({ background: '', profile: '', @@ -62,14 +54,10 @@ const ProfilePhoto = (): JSX.Element => { const formData = new FormData(); formData.set('background', uploadImages.background); formData.set('profile', uploadImages.profile); - uploadImagesAPI(formData).then((data: any): void => { + uploadImagesAPI(formData).then((data): void => { console.log(data); if (data.error) { - if (data.error.message) { - updateSnackBarMessage(data.error.message); - } else { - updateSnackBarMessage(data.error); - } + updateSnackBarMessage(data.error); } else if (data.success) { updateSnackBarMessage(data.success); } else { @@ -88,8 +76,8 @@ const ProfilePhoto = (): JSX.Element => { className={classes.backgroundMedia} component="img" src={ - images.background - ? images.background + userState.background + ? userState.background : `https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRmFJpMlEV-41ZFT8U7iUsMJzaXVIL_hAtT9A&usqp=CAU` } title="Background Picture" @@ -98,7 +86,7 @@ const ProfilePhoto = (): JSX.Element => { @@ -112,9 +100,9 @@ const ProfilePhoto = (): JSX.Element => {