From 41c006c9791024d1fb9659280ca262eb27137c0f Mon Sep 17 00:00:00 2001 From: SimpPoseidon <120441879+TanishqMehrunkarIIPSDAVV@users.noreply.github.com> Date: Tue, 15 Oct 2024 01:23:37 +0530 Subject: [PATCH] image navbar --- package-lock.json | 21 ++++ package.json | 1 + src/Profile/cropImage.js | 45 ++++++++ src/Profile/profile.css | 70 +++++++++++- src/Profile/profile.jsx | 230 +++++++++++++++++++++++++++++++-------- 5 files changed, 319 insertions(+), 48 deletions(-) create mode 100644 src/Profile/cropImage.js diff --git a/package-lock.json b/package-lock.json index 5ac22f7..53ad00e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "react": "^18.3.1", "react-datepicker": "^7.3.0", "react-dom": "^18.3.1", + "react-easy-crop": "^5.1.0", "react-icon": "^1.0.0", "react-model": "^4.3.1", "react-scripts": "5.0.1", @@ -14044,6 +14045,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/normalize-wheel": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz", + "integrity": "sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==", + "license": "BSD-3-Clause" + }, "node_modules/npm": { "version": "10.8.2", "resolved": "https://registry.npmjs.org/npm/-/npm-10.8.2.tgz", @@ -18991,6 +18998,20 @@ "react": ">= 16.8 || 18.0.0" } }, + "node_modules/react-easy-crop": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-5.1.0.tgz", + "integrity": "sha512-UsYeF/N7zoqtfOSD+2xSt1nRaoBYCI2YLkzmq+hi+aVepS4/bAMhbrLwJtDAP60jsVzWRiQCX7JG+ZtfWcHsiw==", + "license": "MIT", + "dependencies": { + "normalize-wheel": "^1.0.1", + "tslib": "^2.0.1" + }, + "peerDependencies": { + "react": ">=16.4.0", + "react-dom": ">=16.4.0" + } + }, "node_modules/react-error-overlay": { "version": "6.0.11", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", diff --git a/package.json b/package.json index 1b94c06..83b734c 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "react": "^18.3.1", "react-datepicker": "^7.3.0", "react-dom": "^18.3.1", + "react-easy-crop": "^5.1.0", "react-icon": "^1.0.0", "react-model": "^4.3.1", "react-scripts": "5.0.1", diff --git a/src/Profile/cropImage.js b/src/Profile/cropImage.js new file mode 100644 index 0000000..2dd0c91 --- /dev/null +++ b/src/Profile/cropImage.js @@ -0,0 +1,45 @@ +const getCroppedImg = (imageSrc, croppedAreaPixels) => { + const createImage = (url) => + new Promise((resolve, reject) => { + const image = new Image(); + image.onload = () => resolve(image); + image.onerror = (error) => reject(error); + image.src = url; + }); + + const getCroppedImg = async (imageSrc, crop) => { + const image = await createImage(imageSrc); + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + + canvas.width = crop.width; + canvas.height = crop.height; + + ctx.drawImage( + image, + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + + return new Promise((resolve) => { + canvas.toBlob((blob) => { + if (!blob) { + console.error("Canvas is empty"); + return; + } + const fileUrl = window.URL.createObjectURL(blob); + resolve(fileUrl); + }, "image/jpeg"); + }); + }; + + return getCroppedImg(imageSrc, croppedAreaPixels); + }; + +export default getCroppedImg; \ No newline at end of file diff --git a/src/Profile/profile.css b/src/Profile/profile.css index d75ea17..fc854d1 100644 --- a/src/Profile/profile.css +++ b/src/Profile/profile.css @@ -16,6 +16,7 @@ .profile-header { position: relative; display: inline-block; + margin-bottom: 30px; } .profile-image { @@ -53,7 +54,7 @@ font-size: 24px; font-weight: bold; margin-bottom: 5px; - margin-top: 50px; + margin-top: 20px; } .profile-mob { @@ -211,3 +212,70 @@ .profile-edit-password-button:hover { background-color: #4caf4fba; } + +.profile-cropper +{ + position: absolute; + top:50%; + left:50%; + transform: translate(-50%,-50%); +} +.profile-crop-button-container +{ + position: relative; + top: 400px; + display: flex; + justify-content: center; + align-items: center; + gap: 50px; +} +.profile-cropper-modal button, .profile-image-submit +{ + width: 150px; + height: 40px; + border-radius: 5px; + border-color: transparent; + font-size: large; + font-weight: 500; +} +.profile-cropper-modal button:hover, .profile-image-submit:hover +{ + cursor: pointer; +} +.profile-button-crop +{ + background-color: #4CAF50; +} +.profile-button-cancel +{ + background-color: #c9302c; +} +.profile-button-crop:hover +{ + background-color: #4caf4f7e; +} +.profile-button-cancel:hover +{ + background-color: #c9312c8c; +} +.profile-cropper-note +{ + position: relative; + color: red; + top: 410px; + font-weight: 600; +} +.profile-image-submit +{ + margin-top: 50px; + width: 200px; + background-color: #4CAF50; +} +.profile-image-submit +{ + background-color: #4caf50; +} +.profile-image-submit:hover +{ + background-color: #4CAF4f7e; +} \ No newline at end of file diff --git a/src/Profile/profile.jsx b/src/Profile/profile.jsx index c83bf98..70f641e 100644 --- a/src/Profile/profile.jsx +++ b/src/Profile/profile.jsx @@ -1,13 +1,15 @@ -import React, { useState, useEffect } from 'react'; -import axios from 'axios'; -import './profile.css'; -import Navbar from '../Navbar/Navbar'; -import { FaPlus, FaEye, FaEyeSlash } from 'react-icons/fa'; -import Modal from 'react-modal'; -import AlertModal from '../AlertModal/AlertModal'; +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import "./profile.css"; +import Navbar from "../Navbar/Navbar"; +import { FaPlus, FaEye, FaEyeSlash } from "react-icons/fa"; +import Modal from "react-modal"; +import AlertModal from "../AlertModal/AlertModal"; import defaultPhoto from "../Assets/profile_photo.png"; +import Cropper from "react-easy-crop"; +import getCroppedImg from "./cropImage"; // Utility to crop the image -Modal.setAppElement('#root'); +Modal.setAppElement("#root"); const Profile = () => { const [profileData, setProfileData] = useState({ @@ -16,7 +18,7 @@ const Profile = () => { email: "", mobile_no: "", password: "", - confirmPassword: "" + confirmPassword: "", }); const [modalIsOpen, setModalIsOpen] = useState(false); @@ -27,28 +29,41 @@ const Profile = () => { const [showPassword, setShowPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false); const [editPassword, setEditPassword] = useState(false); - const [teacher, setTeacher] = useState(); const [passwordsMatch, setPasswordMatch] = useState(false); + const [modalCropIsOpen, setModalCropIsOpen] = useState(false); + const [crop, setCrop] = useState({ x: 0, y: 0 }); + const [zoom, setZoom] = useState(1); + const [croppedAreaPixels, setCroppedAreaPixels] = useState(null); + const [submitDisplay,setSubmitDisplay] = useState(false); useEffect(() => { - axios.post('http://localhost:5000/teacher/getteacherDetails', { teacherId: localStorage.getItem("teacherId") }) - .then((response) => { - setTeacher(response?.data?.teacher); - if (teacher) { - setProfileData(prevData => ({ + // Fetch teacher details only when the component mounts + const fetchTeacherDetails = async () => { + try { + const response = await axios.post("http://localhost:5000/teacher/getteacherDetails", { + teacherId: localStorage.getItem("teacherId"), + }); + + const teacherData = response?.data?.teacher; + if (teacherData) { + setProfileData((prevData) => ({ ...prevData, - name: teacher?.name, - email: teacher?.email, - mobile_no: teacher?.mobileNumber, - password: teacher?.password, - confirmPassword: teacher?.password, + photo: teacherData.profilePic || defaultPhoto, + name: teacherData.name, + email: teacherData.email, + mobile_no: teacherData.mobileNumber, + password: teacherData.password, + confirmPassword: teacherData.password, })); } - }) - .catch((error) => { - console.log(error); - }); - }, ); + } catch (error) { + console.error(error); + } + }; + + fetchTeacherDetails(); + }, []); // Empty dependency array to ensure this effect only runs on mount + const openModal = () => { setNewProfileData({ ...profileData, password: "", confirmPassword: "" }); @@ -70,7 +85,7 @@ const Profile = () => { const handleSave = () => { const { email, mobile_no, password, confirmPassword } = newProfileData; - if (!email.includes('@')) { + if (!email.includes("@")) { openAlertModal("Please enter a valid email address.", true); return; } @@ -82,9 +97,13 @@ const Profile = () => { } if (editPassword) { - const passwordRegex = /^(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/; + const passwordRegex = + /^(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/; if (!passwordRegex.test(password)) { - openAlertModal("Password must be at least 8 characters, contain one uppercase letter, one number, and one special character.", true); + openAlertModal( + "Password must be at least 8 characters, contain one uppercase letter, one number, and one special character.", + true + ); return; } @@ -94,11 +113,11 @@ const Profile = () => { } } - // Merging /edit API call - axios.post('http://localhost:5000/teacher/edit', { - teacherId: localStorage.getItem("teacherId"), - ...newProfileData, - }) + axios + .post("http://localhost:5000/teacher/edit", { + teacherId: localStorage.getItem("teacherId"), + ...newProfileData, + }) .then((response) => { setProfileData({ ...profileData, @@ -115,6 +134,10 @@ const Profile = () => { }); }; + const onCropComplete = (croppedArea, croppedAreaPixels) => { + setCroppedAreaPixels(croppedAreaPixels); + }; + const handleImageChange = (event) => { const file = event.target.files[0]; if (file) { @@ -126,18 +149,109 @@ const Profile = () => { }); }; reader.readAsDataURL(file); + setModalCropIsOpen(true); + setSubmitDisplay(true); + } + }; + + const saveCroppedImage = async () => { + try { + const croppedImage = await getCroppedImg(profileData.photo, croppedAreaPixels); + setProfileData((prevData) => ({ + ...prevData, + photo: croppedImage, + })); + setModalCropIsOpen(false); + } catch (error) { + console.error("Error cropping the image: ", error); } }; const togglePasswordVisibility = () => setShowPassword(!showPassword); - const toggleConfirmPasswordVisibility = () => setShowConfirmPassword(!showConfirmPassword); + const toggleConfirmPasswordVisibility = () => + setShowConfirmPassword(!showConfirmPassword); + + const editProfilePicture = async () => { + try { + const formData = new FormData(); + formData.append("file", profileData.photo); + + // Upload the image to the server (assuming the endpoint is '/paper/upload') + const uploadResponse = await axios.post("http://localhost:5000/paper/upload", formData, { + headers: { + "Content-Type": "multipart/form-data", + }, + }); + + const profilePicUrl = uploadResponse.data.url; // Extract the uploaded image URL + + // Update the teacher's profile picture with the new URL + const response = await axios.post("http://localhost:5000/teacher/editProfilePicture", { + link: profilePicUrl, + teacherId: localStorage.getItem("teacherId"), + }); + + + + // Update state with the new profile picture + setProfileData((prevData) => ({ + ...prevData, + photo: profilePicUrl, // Set the new profile picture URL + })); + + // Show success alert + setAlertIsOpen(true); + setAlertMessage(response.data.message); + } catch (err) { + // Handle error + setIsError(true); + setAlertIsOpen(true); + setAlertMessage("Failed to update profile picture. Please try again."); + console.error(err); + } + }; return ( <> + { + setModalCropIsOpen(false); + }} + contentLabel="Cropper" + className="profile-cropper-modal" + overlayClassName="profile-overlay" + > + +
+
+ + +
+
Note: On pressing Cancel, Image will not be cropped but is saved!!!
+
+
- Profile + Profile @@ -146,15 +260,20 @@ const Profile = () => { type="file" accept="image/*" onChange={handleImageChange} - style={{ display: 'none' }} + style={{ display: "none" }} />
+ { + submitDisplay && + }
{profileData.name}

Email: {profileData.email}

Mobile: {profileData.mobile_no}

- +
{ setNewProfileData({ ...newProfileData, name: e.target.value })} + onChange={(e) => + setNewProfileData({ ...newProfileData, name: e.target.value }) + } /> - @@ -214,9 +346,9 @@ const Profile = () => { ...newProfileData, password: e.target.value, }); - if(e.target.value == newProfileData.confirmPassword){ + if (e.target.value == newProfileData.confirmPassword) { setPasswordMatch(true); - }else{ + } else { setPasswordMatch(false); } }} @@ -245,9 +377,9 @@ const Profile = () => { ...newProfileData, confirmPassword: e.target.value, }); - if(newProfileData.password == e.target.value){ + if (newProfileData.password == e.target.value) { setPasswordMatch(true); - }else{ + } else { setPasswordMatch(false); } }} @@ -264,8 +396,12 @@ const Profile = () => { )}
- - + +