Skip to content

Commit

Permalink
feat: Implement profile picture #343 (#558)
Browse files Browse the repository at this point in the history
Co-authored-by: Dunsin <78784850+Dun-sin@users.noreply.github.com>
  • Loading branch information
hubsMIT1 and Dun-sin authored Nov 7, 2023
1 parent b602f92 commit 15ba8c3
Show file tree
Hide file tree
Showing 10 changed files with 1,254 additions and 38 deletions.
970 changes: 953 additions & 17 deletions client/package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@
"@mui/material": "^5.6.1",
"@mui/styled-engine-sc": "^5.6.1",
"@rsuite/icons": "^1.0.3",
"@tensorflow/tfjs": "^4.12.0",
"axios": "^1.4.0",
"bad-words-next": "^2.2.1",
"crypto-js": "^4.1.1",
"emoji-picker-react": "^4.5.2",
"lodash": "^4.17.21",
"markdown-it": "^13.0.1",
"nsfwjs": "^2.4.2",

This comment has been minimized.

Copy link
@sarthakgarg814

sarthakgarg814 Dec 8, 2023

Causing dependency issues

"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.10.1",
Expand Down
38 changes: 38 additions & 0 deletions client/src/components/UserAvatar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { HiUserCircle, HiPencil } from 'react-icons/hi';
import PropTypes from 'prop-types';


export default function UserAvatar({imageRef, onUploadClick, onEditClick }) {
return (
<div className="relative group">
{imageRef?.current.src ? (
<>
<img
src={imageRef.current.src}
ref={imageRef}
alt="Profile Image"
className="h-20 w-20 rounded-full"
/>
<div className="absolute top-0 -right-5 opacity-0 group-hover:opacity-100 flex items-center space-x-2">
<HiPencil
className="text-blue-500 h-6 w-6 cursor-pointer"
onClick={onEditClick}
/>
</div>
</>
) : (
<HiUserCircle
className="text-highlight h-20 w-20 cursor-pointer"
onClick={onUploadClick}
title='upload profile image'
/>
)}
</div>
)
};

UserAvatar.propTypes = {
imageRef: PropTypes.object,
onUploadClick: PropTypes.func,
onEditClick: PropTypes.func,
};
105 changes: 86 additions & 19 deletions client/src/pages/Profile.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useEffect, useRef, useState } from 'react';

import { HiUserCircle } from 'react-icons/hi';
import UserAvatar from '../components/UserAvatar';
import { useKindeAuth } from '@kinde-oss/kinde-auth-react';
import * as nsfwjs from 'nsfwjs';

import { useAuth } from 'context/AuthContext';

Expand All @@ -12,22 +13,28 @@ import SignupAnonUser from './SignupAnonUser';
const Profile = () => {
const [username, setUsername] = useState('Anonymous');
const [profileResponse, setProfileResponse] = useState();

const [imageFile, setImageFile] = useState(null);
const [isImageSafe,setImageSafe] = useState(false);
const { authState, dispatchAuth } = useAuth();
const [loading, setLoading] = useState(false);
const { logout } = useKindeAuth();

const aboutRef = useRef(null);
const genderRef = useRef(null);
const ageRef = useRef(null);
const imageRef = useRef(new Image());

const { email } = authState;

const getProfileData = async (email) => {
try {
const response = await api.get(`/profile/${email}`);
const { aboutMe, age, gender, username } = response.data;
const { aboutMe, age, gender, username, profileImage } = response.data;

setUsername(username);
if (profileImage) {
imageRef.current.src = profileImage
}
aboutRef.current.value = aboutMe || '';
ageRef.current.value = age;
genderRef.current.value = gender;
Expand All @@ -37,23 +44,77 @@ const Profile = () => {
};

const handleUserName = (e) => setUsername(e.target.value);

const handleUpdateProfile = async () => {
const data = {
email,
username,
aboutMe: aboutRef.current.value,
gender: genderRef.current.value,
age: Number(ageRef.current.value),
const handlerisImageSafe =async()=>{
setLoading(true);
try {
const model = await nsfwjs.load();
const predictions = await model.classify(imageRef.current);
const neutralProb = predictions.find((p) => p.className === 'Neutral');
return neutralProb.probability>=0.6;
} catch (error) {
setProfileResponse("Profile image update is temporarily unavailable. Please try again later.")
console.error('Error classifying image:', error);
return false;
}
};
const handleImageUpload = () => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.onchange = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = async (event) => {
imageRef.current.src = event.target.result;
setLoading(true);
const imageSafe = await handlerisImageSafe();
imageSafe?setProfileResponse()
:setProfileResponse('Profile image is not safe. Please upload a different image.');

setImageSafe(imageSafe)
setImageFile(file);
setLoading(false);
};
reader.readAsDataURL(file);
}
};

fileInput.click();
};
const handleEditProfile = () => {
handleImageUpload();
};
const handleUpdateProfile = async () => {
if(loading){
return;
}
const formData = new FormData();
formData.append('email', email);
formData.append('username', username);
formData.append('aboutMe', aboutRef.current.value);
formData.append('gender', genderRef.current.value);
formData.append('age', Number(ageRef.current.value));

if(imageFile && isImageSafe){
formData.append('profileImage',imageFile)
}
setLoading(true);
try {
const response = await api.post('/profile', data);
const response = await api.post('/profile', formData);
console.log(response.data.message);
setProfileResponse(response.data);
setProfileResponse(response.data.message);

} catch (error) {
console.error(error);
// Handle network errors or other unexpected issues
console.error('Error uploading file:', error);
if (error.response) {
// Handle HTTP errors with custom messages
console.log(error.response.data.error);
setProfileResponse(error.response.data.error);
}
}
setLoading(false);

};

function logOut() {
Expand Down Expand Up @@ -104,7 +165,11 @@ const Profile = () => {
) : (
<>
<section className="min-w-[300px] max-w-[400px] w-[40%] px-10 py-8 rounded-2xl flex flex-col justify-center items-center bg-clip-padding backdrop-filter backdrop-blur-2xl bg-gray-100 dark:bg-opacity-5 dark:bg-gray-300">
<HiUserCircle className="text-highlight h-20 w-20" />
<UserAvatar
imageRef={imageRef}
onUploadClick={handleImageUpload}
onEditClick={handleEditProfile}
/>

<input
className="outline-none bg-transparent w-fit text-center text-2xl placeholder:text-2xl placeholder:text-white"
Expand Down Expand Up @@ -158,18 +223,20 @@ const Profile = () => {
<button
className="border min-w-[300px] max-w-[400px] w-[40%] p-2 text-md rounded-xl border-green-500 text-green-500 hover:bg-green-500 hover:text-white dark:border-green-500 dark:text-green-500 dark:hover:bg-green-500 dark:hover:text-white"
onClick={handleUpdateProfile}
disabled={loading}
>
Save changes
{loading? `Processing...` : `Save changes`}
</button>
<button
className="border min-w-[300px] max-w-[400px] w-[40%] p-2 text-md rounded-xl border-red text-red hover:bg-red hover:text-white"
onClick={handleDeleteAccount}
disabled={loading}
>
Delete My Account
</button>
{profileResponse ? (
<div>
<p className="text-green-300">Profile Updated!</p>
<p className="text-green-300">{profileResponse}</p>
</div>
) : null}
</>
Expand Down
1 change: 1 addition & 0 deletions client/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default defineConfig({
context: resolve(__dirname, './src/context'),
styles: resolve(__dirname, './src/styles'),
assets: resolve(__dirname, './src/assets'),
nsfwjs: 'nsfwjs/dist/nsfwjs.min.js',
},
},
});
2 changes: 1 addition & 1 deletion server/.env_sample
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ THIS IS JUST A FILE TO HELP YOU KNOW WHAT IS NEEDED IN THE .env FILE.
YOU HAVE TO CREATE ANOTHER FILE AND NAME IT .env

MongoDB_URL=mongodb://username:password@localhost:27018/
SECRET_KEY=decryption101
SECRET_KEY=decryption101
10 changes: 9 additions & 1 deletion server/controllers/userController.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ const UserRouter = require('express').Router();
const validator = require('validator').default;
const { v4: uuidv4 } = require('uuid');

const multer = require('multer');
// multer for profile image
const storage = multer.memoryStorage();
const imageUpload = multer({ storage: storage });

const User = require('../models/UserSchema');

let accessToken = process.env.ACCESS_TOKEN;
Expand Down Expand Up @@ -222,6 +227,9 @@ const updateProfile = async (req, res) => {
user.gender = gender || user.gender || 'Unknown';
user.age = age || user.age || null;
user.settings = settings || user.settings;
user.profileImage = req.file
? `data:${req.file.mimetype};base64,${req.file.buffer.toString('base64')}`
: user.profileImage;

// Save the updated user profile
await user.save();
Expand Down Expand Up @@ -275,7 +283,7 @@ const deleteUser = async (req, res) => {
};

UserRouter.route('/login').post(emailValidator, loginUser);
UserRouter.route('/profile').post(emailValidator, updateProfile);
UserRouter.route('/profile').post(imageUpload.single('profileImage'), emailValidator, updateProfile);
UserRouter.route('/profile/:email').get(getProfile);
UserRouter.route('/deleteUser').delete(emailValidator, deleteUser); //Email validation applied to the required request handlers

Expand Down
4 changes: 4 additions & 0 deletions server/models/UserSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ const UserSchema = new mongoose.Schema({
type: Object,
required: false,
},
profileImage: {
type: String,
required: false
},
});

UserSchema.index({ email: 1 }, { unique: true });
Expand Down
Loading

1 comment on commit 15ba8c3

@sarthakgarg814
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Dun-sin,
Can you check on client side its facing issue and also if you see here vercel build also failed

Please sign in to comment.