From 790731224dd42fc7b3f1038e40eee60837514ff7 Mon Sep 17 00:00:00 2001 From: adityapat24 Date: Mon, 9 Mar 2026 18:26:37 -0400 Subject: [PATCH 1/5] adding profile pic, frontend needs fix --- frontend/package-lock.json | 19 ++ frontend/package.json | 5 +- frontend/src/components/Avatar.tsx | 26 ++ .../grants/grant-view/ContactCard.tsx | 8 +- .../settings/ProfilePictureModal.tsx | 262 ++++++++++++++++++ frontend/src/main-page/settings/Settings.tsx | 67 ++++- frontend/src/main-page/settings/cropUtils.ts | 55 ++++ .../settings/profilePictureConstants.ts | 16 ++ .../src/main-page/users/user-rows/UserRow.tsx | 8 +- 9 files changed, 443 insertions(+), 23 deletions(-) create mode 100644 frontend/src/components/Avatar.tsx create mode 100644 frontend/src/main-page/settings/ProfilePictureModal.tsx create mode 100644 frontend/src/main-page/settings/cropUtils.ts create mode 100644 frontend/src/main-page/settings/profilePictureConstants.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e455fdc8..c9dd8c89 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -27,6 +27,7 @@ "react-currency-input-field": "^4.0.3", "react-datepicker": "^8.2.1", "react-dom": "^18.3.1", + "react-easy-crop": "^5.5.6", "react-icons": "^5.4.0", "react-router-dom": "^6.26.2", "react-transition-group": "^4.4.5", @@ -10349,6 +10350,11 @@ "node": ">=0.10.0" } }, + "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==" + }, "node_modules/nullthrows": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", @@ -11243,6 +11249,19 @@ "react": "^18.3.1" } }, + "node_modules/react-easy-crop": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-5.5.6.tgz", + "integrity": "sha512-Jw3/ozs8uXj3NpL511Suc4AHY+mLRO23rUgipXvNYKqezcFSYHxe4QXibBymkOoY6oOtLVMPO2HNPRHYvMPyTw==", + "dependencies": { + "normalize-wheel": "^1.0.1", + "tslib": "^2.0.1" + }, + "peerDependencies": { + "react": ">=16.4.0", + "react-dom": ">=16.4.0" + } + }, "node_modules/react-icons": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 8f8cbd1c..c0e5558b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -30,6 +30,7 @@ "react-currency-input-field": "^4.0.3", "react-datepicker": "^8.2.1", "react-dom": "^18.3.1", + "react-easy-crop": "^5.5.6", "react-icons": "^5.4.0", "react-router-dom": "^6.26.2", "react-transition-group": "^4.4.5", @@ -54,11 +55,11 @@ "globals": "^15.9.0", "jsdom": "^25.0.1", "postcss": "^8.5.3", + "tailwindcss": "^3.4.17", "typescript": "^5.7.3", "typescript-eslint": "^8.0.1", "vite": "^5.4.8", "vite-tsconfig-paths": "^5.1.4", - "vitest": "^2.1.8", - "tailwindcss": "^3.4.17" + "vitest": "^2.1.8" } } diff --git a/frontend/src/components/Avatar.tsx b/frontend/src/components/Avatar.tsx new file mode 100644 index 00000000..5d908c3e --- /dev/null +++ b/frontend/src/components/Avatar.tsx @@ -0,0 +1,26 @@ +import { useState } from "react"; + +type AvatarProps = { + src: string | null | undefined; + alt: string; + className?: string; + fallbackSrc: string; +}; + +/** + * Renders a profile image with fallback when the URL fails to load. + * Use for profile pictures and avatars across the app. + */ +export default function Avatar({ src, alt, className = "", fallbackSrc }: AvatarProps) { + const [imgError, setImgError] = useState(false); + const effectiveSrc = src && !imgError ? src : fallbackSrc; + + return ( + {alt} setImgError(true)} + /> + ); +} diff --git a/frontend/src/main-page/grants/grant-view/ContactCard.tsx b/frontend/src/main-page/grants/grant-view/ContactCard.tsx index c65505f2..d565a393 100644 --- a/frontend/src/main-page/grants/grant-view/ContactCard.tsx +++ b/frontend/src/main-page/grants/grant-view/ContactCard.tsx @@ -1,5 +1,6 @@ import { getAppStore } from "../../../external/bcanSatchel/store"; import POC from "../../../../../middle-layer/types/POC"; +import Avatar from "../../../components/Avatar"; import logo from "../../../images/logo.svg"; type ContactCardProps = { @@ -20,10 +21,11 @@ const contactPhoto = return (
- Profile

diff --git a/frontend/src/main-page/settings/ProfilePictureModal.tsx b/frontend/src/main-page/settings/ProfilePictureModal.tsx new file mode 100644 index 00000000..89797dbe --- /dev/null +++ b/frontend/src/main-page/settings/ProfilePictureModal.tsx @@ -0,0 +1,262 @@ +import { useState, useCallback } from "react"; +import Cropper, { Area } from "react-easy-crop"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faXmark } from "@fortawesome/free-solid-svg-icons"; +import Button from "../../components/Button"; +import { getCroppedImg } from "./cropUtils"; +import { + ALLOWED_PROFILE_PIC_MIME_TYPES, + ALLOWED_PROFILE_PIC_EXTENSIONS, + MAX_PROFILE_PIC_SIZE_BYTES, + MAX_PROFILE_PIC_SIZE_MB, +} from "./profilePictureConstants"; +import { api } from "../../api"; +import { getAppStore } from "../../external/bcanSatchel/store"; +import { updateUserProfile } from "../../external/bcanSatchel/actions"; +import { setActiveUsers } from "../../external/bcanSatchel/actions"; +import { User } from "../../../../middle-layer/types/User"; + +type ProfilePictureModalProps = { + isOpen: boolean; + onClose: () => void; + onSuccess?: () => void; + onError?: (message: string) => void; +}; + +export default function ProfilePictureModal({ + isOpen, + onClose, + onSuccess, + onError, +}: ProfilePictureModalProps) { + const [imageSrc, setImageSrc] = useState(null); + const [crop, setCrop] = useState({ x: 0, y: 0 }); + const [zoom, setZoom] = useState(1); + const [croppedAreaPixels, setCroppedAreaPixels] = useState(null); + const [uploadError, setUploadError] = useState(null); + const [isUploading, setIsUploading] = useState(false); + const [validationError, setValidationError] = useState(null); + + const user = getAppStore().user; + + const onCropComplete = useCallback((_croppedArea: Area, croppedAreaPixels: Area) => { + setCroppedAreaPixels(croppedAreaPixels); + }, []); + + const handleClose = () => { + setImageSrc(null); + setCrop({ x: 0, y: 0 }); + setZoom(1); + setCroppedAreaPixels(null); + setUploadError(null); + setValidationError(null); + setIsUploading(false); + onClose(); + }; + + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + e.target.value = ""; + + setValidationError(null); + setUploadError(null); + + if (!file) return; + + if (!ALLOWED_PROFILE_PIC_MIME_TYPES.includes(file.type as (typeof ALLOWED_PROFILE_PIC_MIME_TYPES)[number])) { + setValidationError( + `Invalid file type. Allowed: ${ALLOWED_PROFILE_PIC_EXTENSIONS.join(", ")}` + ); + return; + } + + if (file.size > MAX_PROFILE_PIC_SIZE_BYTES) { + setValidationError(`File too large. Maximum size is ${MAX_PROFILE_PIC_SIZE_MB} MB.`); + return; + } + + const reader = new FileReader(); + reader.addEventListener("load", () => { + setImageSrc(reader.result as string); + }); + reader.readAsDataURL(file); + }; + + const handleSave = async () => { + if (!imageSrc || !croppedAreaPixels || !user) return; + + setIsUploading(true); + setUploadError(null); + + try { + const blob = await getCroppedImg(imageSrc, croppedAreaPixels); + const formData = new FormData(); + formData.append("profilePic", blob, "profilepic.jpg"); + formData.append( + "user", + JSON.stringify({ + firstName: user.firstName, + lastName: user.lastName, + email: user.email, + position: user.position, + }) + ); + + const response = await api("/user/upload-pfp", { + method: "POST", + body: formData, + }); + + if (!response.ok) { + const errBody = await response.json().catch(() => ({})); + const message = + (errBody as { message?: string }).message || + `Upload failed (${response.status})`; + throw new Error(message); + } + + const raw = await response.text(); + let url: string; + try { + const parsed = JSON.parse(raw); + url = typeof parsed === "string" ? parsed : String(parsed); + } catch { + url = raw.replace(/^"|"$/g, "").trim(); + } + + updateUserProfile({ ...user, profilePicUrl: url }); + + const store = getAppStore(); + const updatedActiveUsers = (store.activeUsers || []).map((u: User) => + u.email === user.email ? { ...u, profilePicUrl: url } : u + ); + setActiveUsers(updatedActiveUsers); + + handleClose(); + onSuccess?.(); + } catch (err) { + const message = err instanceof Error ? err.message : "Failed to upload profile picture."; + setUploadError(message); + onError?.(message); + } finally { + setIsUploading(false); + } + }; + + if (!isOpen) return null; + + return ( +

+
+
+

+ Profile Picture +

+ +
+ + {!imageSrc ? ( +
+ + {validationError && ( +
+ {validationError} +
+ )} +
+ ) : ( + <> +
+ +
+ +
+ + setZoom(Number(e.target.value))} + className="w-full h-2 rounded-lg appearance-none cursor-pointer bg-grey-300 accent-primary-900" + /> +
{zoom.toFixed(1)}x
+
+ + {(uploadError || validationError) && ( +
+ {uploadError ?? validationError} +
+ )} + +
+
+
+ + )} +
+
+ ); +} diff --git a/frontend/src/main-page/settings/Settings.tsx b/frontend/src/main-page/settings/Settings.tsx index e2964643..58075190 100644 --- a/frontend/src/main-page/settings/Settings.tsx +++ b/frontend/src/main-page/settings/Settings.tsx @@ -1,25 +1,39 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; +import { observer } from "mobx-react-lite"; import Button from "../../components/Button"; import InfoCard from "./components/InfoCard"; +import Avatar from "../../components/Avatar"; import logo from "../../images/logo.svg"; import { faPenToSquare } from "@fortawesome/free-solid-svg-icons"; import ChangePasswordModal from "./ChangePasswordModal"; +import ProfilePictureModal from "./ProfilePictureModal"; +import { getAppStore } from "../../external/bcanSatchel/store"; +import { ALLOWED_PROFILE_PIC_EXTENSIONS, MAX_PROFILE_PIC_SIZE_MB } from "./profilePictureConstants"; const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; -const initialPersonalInfo = { - firstName: "John", - lastName: "Doe", - email: "john.doe@gmail.com", -}; +function Settings() { + const user = getAppStore().user; + const personalInfoFromUser = user + ? { firstName: user.firstName, lastName: user.lastName, email: user.email } + : { firstName: "", lastName: "", email: "" }; -export default function Settings() { - const [personalInfo, setPersonalInfo] = useState(initialPersonalInfo); + const [personalInfo, setPersonalInfo] = useState(personalInfoFromUser); const [isEditingPersonalInfo, setIsEditingPersonalInfo] = useState(false); - const [editForm, setEditForm] = useState(initialPersonalInfo); + const [editForm, setEditForm] = useState(personalInfoFromUser); const [personalInfoError, setPersonalInfoError] = useState(null); const [isChangePasswordModalOpen, setIsChangePasswordModalOpen] = useState(false); const [changePasswordError, setChangePasswordError] = useState(null); + const [isProfilePictureModalOpen, setIsProfilePictureModalOpen] = useState(false); + const [profilePictureMessage, setProfilePictureMessage] = useState<{ type: "success" | "error"; text: string } | null>(null); + + useEffect(() => { + if (user) { + const next = { firstName: user.firstName, lastName: user.lastName, email: user.email }; + setPersonalInfo((prev) => (prev.email === user.email ? prev : next)); + setEditForm((prev) => (prev.email === user.email ? prev : next)); + } + }, [user]); const handleStartEdit = () => { setEditForm(personalInfo); @@ -50,34 +64,56 @@ export default function Settings() {
- Profile

Profile Picture

+ {profilePictureMessage && ( +
+ {profilePictureMessage.text} +
+ )}

- We support PNGs, JPEGs, and PDFs under 10 MB + {ALLOWED_PROFILE_PIC_EXTENSIONS.join(", ")} up to {MAX_PROFILE_PIC_SIZE_MB} MB

+ setIsProfilePictureModalOpen(false)} + onSuccess={() => setProfilePictureMessage({ type: "success", text: "Profile picture updated." })} + onError={(msg) => setProfilePictureMessage({ type: "error", text: msg })} + /> + setIsChangePasswordModalOpen(false)} error={changePasswordError} onSubmit={(values) => { - // Backend: call API with values.currentPassword and values.newPassword void values; }} />
); } + +export default observer(Settings); diff --git a/frontend/src/main-page/settings/cropUtils.ts b/frontend/src/main-page/settings/cropUtils.ts new file mode 100644 index 00000000..d0a534c0 --- /dev/null +++ b/frontend/src/main-page/settings/cropUtils.ts @@ -0,0 +1,55 @@ +/** + * Creates a cropped image blob from the source image and crop area. + * Used with react-easy-crop's onCropComplete (croppedAreaPixels). + */ +export async function getCroppedImg( + imageSrc: string, + pixelCrop: { x: number; y: number; width: number; height: number } +): Promise { + const image = await createImage(imageSrc); + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + + if (!ctx) { + throw new Error("No 2d context"); + } + + canvas.width = pixelCrop.width; + canvas.height = pixelCrop.height; + + ctx.drawImage( + image, + pixelCrop.x, + pixelCrop.y, + pixelCrop.width, + pixelCrop.height, + 0, + 0, + pixelCrop.width, + pixelCrop.height + ); + + return new Promise((resolve, reject) => { + canvas.toBlob( + (blob) => { + if (!blob) { + reject(new Error("Canvas toBlob failed")); + return; + } + resolve(blob); + }, + "image/jpeg", + 0.92 + ); + }); +} + +function createImage(url: string): Promise { + return new Promise((resolve, reject) => { + const image = new Image(); + image.addEventListener("load", () => resolve(image)); + image.addEventListener("error", (error) => reject(error)); + image.setAttribute("crossOrigin", "anonymous"); + image.src = url; + }); +} diff --git a/frontend/src/main-page/settings/profilePictureConstants.ts b/frontend/src/main-page/settings/profilePictureConstants.ts new file mode 100644 index 00000000..59165e3d --- /dev/null +++ b/frontend/src/main-page/settings/profilePictureConstants.ts @@ -0,0 +1,16 @@ +/** Allowed MIME types for profile picture upload (must match backend). */ +export const ALLOWED_PROFILE_PIC_MIME_TYPES = [ + "image/jpeg", + "image/jpg", + "image/png", + "image/gif", + "image/webp", +] as const; + +/** Allowed file extensions for display in UI. */ +export const ALLOWED_PROFILE_PIC_EXTENSIONS = ["JPG", "JPEG", "PNG", "GIF", "WEBP"]; + +/** Max file size: 5MB (backend limit). */ +export const MAX_PROFILE_PIC_SIZE_BYTES = 5 * 1024 * 1024; + +export const MAX_PROFILE_PIC_SIZE_MB = 5; diff --git a/frontend/src/main-page/users/user-rows/UserRow.tsx b/frontend/src/main-page/users/user-rows/UserRow.tsx index 1cd1d967..481cf90c 100644 --- a/frontend/src/main-page/users/user-rows/UserRow.tsx +++ b/frontend/src/main-page/users/user-rows/UserRow.tsx @@ -1,5 +1,6 @@ import { User } from "../../../../../middle-layer/types/User"; import UserPositionCard from "./UserPositionCard"; +import Avatar from "../../../components/Avatar"; import logo from "../../../images/logo.svg"; interface UserRowProps { @@ -13,10 +14,11 @@ const UserRow = ({ user, action }: UserRowProps) => { className="grid grid-cols-2 md:grid-cols-[30%_35%_25%_10%] cols gap-2 md:gap-0 text-sm lg:text-base border-b-2 border-grey-150 py-4 px-8 items-center" >
- Profile {user.firstName} {user.lastName}
From 1e530febf8577d3e25594f973e2d48ec1c7a6ffe Mon Sep 17 00:00:00 2001 From: adityapat24 Date: Tue, 10 Mar 2026 11:18:16 -0400 Subject: [PATCH 2/5] fix frontend for zoom --- frontend/src/main-page/settings/ProfilePictureModal.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/main-page/settings/ProfilePictureModal.tsx b/frontend/src/main-page/settings/ProfilePictureModal.tsx index 89797dbe..24725c79 100644 --- a/frontend/src/main-page/settings/ProfilePictureModal.tsx +++ b/frontend/src/main-page/settings/ProfilePictureModal.tsx @@ -31,7 +31,7 @@ export default function ProfilePictureModal({ }: ProfilePictureModalProps) { const [imageSrc, setImageSrc] = useState(null); const [crop, setCrop] = useState({ x: 0, y: 0 }); - const [zoom, setZoom] = useState(1); + const [zoom, setZoom] = useState(1.6); const [croppedAreaPixels, setCroppedAreaPixels] = useState(null); const [uploadError, setUploadError] = useState(null); const [isUploading, setIsUploading] = useState(false); @@ -46,7 +46,7 @@ export default function ProfilePictureModal({ const handleClose = () => { setImageSrc(null); setCrop({ x: 0, y: 0 }); - setZoom(1); + setZoom(1.6); setCroppedAreaPixels(null); setUploadError(null); setValidationError(null); @@ -193,16 +193,16 @@ export default function ProfilePictureModal({ ) : ( <> -
+
From 28dd3456fbb18da177b4d3db5ede986d1340542c Mon Sep 17 00:00:00 2001 From: prooflesben Date: Sat, 14 Mar 2026 18:22:42 -0400 Subject: [PATCH 3/5] Added in remove profile pic functionality --- backend/src/user/user.controller.ts | 7 ++ backend/src/user/user.service.ts | 115 +++++++++++++++++- frontend/package-lock.json | 77 +++++------- frontend/src/external/bcanSatchel/actions.ts | 4 + frontend/src/external/bcanSatchel/mutators.ts | 21 +++- frontend/src/main-page/settings/Settings.tsx | 25 +++- 6 files changed, 199 insertions(+), 50 deletions(-) diff --git a/backend/src/user/user.controller.ts b/backend/src/user/user.controller.ts index e4338961..b13a31a9 100644 --- a/backend/src/user/user.controller.ts +++ b/backend/src/user/user.controller.ts @@ -321,4 +321,11 @@ async uploadProfilePic( throw new BadRequestException('Invalid user data format'); } } + +@Post('remove-pfp') +async removeProfilePic( + @Body('email') email: string +) { + return await this.userService.removeProfilePicture(email); +} } diff --git a/backend/src/user/user.service.ts b/backend/src/user/user.service.ts index 5ca889a5..388b1d48 100644 --- a/backend/src/user/user.service.ts +++ b/backend/src/user/user.service.ts @@ -68,7 +68,7 @@ async uploadProfilePic(user: User, pic: Express.Multer.File): Promise { } this.logger.log(`✅ Profile picture uploaded successfully for user ${user.email}`); - return updateResult.Attributes.profilePicUrl; + return updateResult.Attributes.profilePicUrl + `?t=${Date.now()}`; } catch (error: any) { this.logger.error(`Failed to upload profile picture for ${user.email}:`, error); @@ -850,9 +850,122 @@ async getAllActiveUsers(): Promise { } } + async removeProfilePicture(email: string): Promise { + const tableName = process.env.DYNAMODB_USER_TABLE_NAME; + let s3Key; + + if (!email || email.trim().length === 0) { + this.logger.error("Remove Profile Picture failed: Email is required"); + throw new BadRequestException("Email is required"); + } + + if (!tableName) { + this.logger.error("DynamoDB User Table Name is not defined in environment variables."); + throw new InternalServerErrorException("Server configuration error"); + } + + try { + // 1. Get the user from DynamoDB to find the current profile picture URL + const existingUserResult = await this.dynamoDb + .get({ + TableName: tableName, + Key: { email }, + }) + .promise(); + + if (!existingUserResult.Item) { + this.logger.error(`User not found in DynamoDB for email: ${email}`); + throw new BadRequestException("User not found in database"); + } + + const existingUser = existingUserResult.Item as User; + + if (!existingUser.profilePicUrl) { + this.logger.log(`User ${email} has no profile picture to remove`); + throw new BadRequestException("User does not have a profile picture"); + } + + // 2. Extract S3 key from the stored URL + // e.g. https://bucket.s3.amazonaws.com/John-Doe-abc-profilepic.jpg → John-Doe-abc-profilepic.jpg + s3Key = decodeURIComponent( + new URL(existingUser.profilePicUrl).pathname.slice(1) + ); + + this.logger.log(`Removing profile picture for ${email}, S3 key: ${s3Key}`); + + // 3. Delete from S3 + await this.s3 + .deleteObject({ + Bucket: this.profilePicBucket, + Key: s3Key, + }) + .promise(); + + this.logger.log(`✓ Profile picture deleted from S3 for ${email}`); + + // 4. Remove the profilePicUrl from DynamoDB + const updateResult = await this.dynamoDb + .update({ + TableName: tableName, + Key: { email }, + UpdateExpression: "REMOVE profilePicUrl", + ReturnValues: "ALL_NEW", + }) + .promise(); + + if (!updateResult.Attributes) { + this.logger.error(`DynamoDB update did not return updated attributes for ${email}`); + throw new InternalServerErrorException("Failed to retrieve updated user data"); + } + + this.logger.log(`✅ Profile picture removed successfully for user ${email}`); + return "Profile picture removed successfully"; + } catch (error: any) { + this.logger.error(`Failed to remove profile picture for ${email}:`, error); + + // Handle S3 errors + if (error.code === "NoSuchBucket") { + this.logger.error(`S3 bucket does not exist: ${this.profilePicBucket}`); + throw new InternalServerErrorException("Storage bucket not found"); + } else if (error.code === "AccessDenied") { + this.logger.error("Access denied to S3 bucket"); + throw new InternalServerErrorException("Insufficient permissions to delete file"); + } else if (error.code === "NoSuchKey") { + // S3 file already gone — still clean up DynamoDB to stay in sync + this.logger.warn(`S3 object not found for key: ${s3Key ?? "unknown"}, cleaning up DynamoDB`); + await this.dynamoDb + .update({ + TableName: tableName, + Key: { email }, + UpdateExpression: "REMOVE profilePicUrl", + ReturnValues: "NONE", + }) + .promise(); + return "Profile picture removed successfully"; + } + + // Handle DynamoDB errors + if (error.code === "ResourceNotFoundException") { + this.logger.error("DynamoDB table does not exist"); + throw new InternalServerErrorException("Database table not found"); + } else if (error.code === "ValidationException") { + this.logger.error("Invalid DynamoDB update parameters"); + throw new BadRequestException("Invalid update parameters"); + } + + if (error instanceof HttpException) { + throw error; + } + + this.logger.error(`Failed to remove profile pic error: ${error}`); + throw new InternalServerErrorException("Failed to remove profile picture"); + } + } + // Helper method for email validation private isValidEmail(email: string): boolean { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } + } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c9dd8c89..e3fada70 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -288,6 +288,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -706,6 +707,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -729,6 +731,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -739,7 +742,6 @@ "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -802,6 +804,7 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1515,7 +1518,6 @@ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.0.1.tgz", "integrity": "sha512-0VpNtO5cNe1/HQWMkl4OdncYK/mv9hnBte0Ew0n6DMzmo3Q3WzDFABHm6LeNTipt5zAyhQ6Ugjiu8aLaEjh1gg==", "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -2578,7 +2580,6 @@ "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=18" } @@ -2595,7 +2596,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -2618,7 +2618,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -2641,7 +2640,6 @@ "os": [ "darwin" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2658,7 +2656,6 @@ "os": [ "darwin" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2675,7 +2672,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2692,7 +2688,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2709,7 +2704,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2726,7 +2720,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2743,7 +2736,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2760,7 +2752,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2777,7 +2768,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2794,7 +2784,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -2817,7 +2806,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -2840,7 +2828,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -2863,7 +2850,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -2886,7 +2872,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -2909,7 +2894,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -2932,7 +2916,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -2952,7 +2935,6 @@ ], "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, - "peer": true, "dependencies": { "@emnapi/runtime": "^1.5.0" }, @@ -2975,7 +2957,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -2995,7 +2976,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3015,7 +2995,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3049,6 +3028,7 @@ "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.9.0.tgz", "integrity": "sha512-yaN3brAnHRD+4KyyOsJyk49XUvj2wtbNACSqg0bz3u8t2VuzhC8Q5dfRnrSxjnnbDb+ienBnkn1TzQfE154vyg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@swc/helpers": "^0.5.0" } @@ -3214,8 +3194,7 @@ "version": "15.5.3", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.3.tgz", "integrity": "sha512-RSEDTRqyihYXygx/OJXwvVupfr9m04+0vH8vyy0HfZ7keRto6VX9BbEk0J2PUk0VGy6YhklJUSrgForov5F9pw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { "version": "15.5.3", @@ -3229,7 +3208,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3246,7 +3224,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3263,7 +3240,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3280,7 +3256,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3297,7 +3272,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3314,7 +3288,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3331,7 +3304,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3348,7 +3320,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3399,6 +3370,7 @@ "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", @@ -3704,6 +3676,7 @@ "resolved": "https://registry.npmjs.org/@pothos/core/-/core-3.41.2.tgz", "integrity": "sha512-iR1gqd93IyD/snTW47HwKSsRCrvnJaYwjVNcUG8BztZPqMxyJKPAnjPHAgu1XB82KEdysrNqIUnXqnzZIs08QA==", "license": "ISC", + "peer": true, "peerDependencies": { "graphql": ">=15.1.0" } @@ -4153,6 +4126,7 @@ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -4410,6 +4384,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.17.tgz", "integrity": "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4431,6 +4406,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz", "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -4442,6 +4418,7 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -4538,6 +4515,7 @@ "integrity": "sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.44.1", "@typescript-eslint/types": "8.44.1", @@ -5880,6 +5858,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6396,6 +6375,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", @@ -6767,8 +6747,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/cliui": { "version": "8.0.1", @@ -7209,7 +7188,8 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.3.tgz", "integrity": "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/date-fns": { "version": "4.1.0", @@ -7653,6 +7633,7 @@ "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8603,6 +8584,7 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz", "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==", "license": "MIT", + "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -8775,6 +8757,7 @@ "resolved": "https://registry.npmjs.org/graphql-yoga/-/graphql-yoga-5.16.0.tgz", "integrity": "sha512-/R2dJea7WgvNlXRU4F8iFwWd95Qn1mN+R+yC8XBs1wKjUzr0Pvv8cGYtt6UUcVHw5CiDEtu7iQY5oOe3sDAWCQ==", "license": "MIT", + "peer": true, "dependencies": { "@envelop/core": "^5.3.0", "@envelop/instrumentation": "^1.0.0", @@ -9497,6 +9480,7 @@ "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssstyle": "^4.1.0", "data-urls": "^5.0.0", @@ -10010,6 +9994,7 @@ "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.13.7.tgz", "integrity": "sha512-aChaVU/DO5aRPmk1GX8L+whocagUUpBQqoPtJk+cm7UOXUk87J4PeWCh6nNmTTIfEhiR9DI/+FnA8dln/hTK7g==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/mobx" @@ -10153,7 +10138,6 @@ "resolved": "https://registry.npmjs.org/next/-/next-15.5.3.tgz", "integrity": "sha512-r/liNAx16SQj4D+XH/oI1dlpv9tdKJ6cONYPwwcCC46f2NjpaRWY+EKCzULfgQYV6YKXjHBchff2IZBSlZmJNw==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "15.5.3", "@swc/helpers": "0.5.15", @@ -10206,7 +10190,6 @@ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -10230,7 +10213,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", @@ -10822,6 +10804,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -11282,6 +11265,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -11446,7 +11430,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -11895,7 +11880,6 @@ "hasInstallScript": true, "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.0", @@ -11938,7 +11922,6 @@ "integrity": "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==", "license": "Apache-2.0", "optional": true, - "peer": true, "engines": { "node": ">=8" } @@ -11949,7 +11932,6 @@ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "optional": true, - "peer": true, "bin": { "semver": "bin/semver.js" }, @@ -12366,7 +12348,6 @@ "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", "license": "MIT", - "peer": true, "dependencies": { "client-only": "0.0.1" }, @@ -12810,6 +12791,7 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13007,6 +12989,7 @@ "resolved": "https://registry.npmjs.org/urql/-/urql-4.2.2.tgz", "integrity": "sha512-3GgqNa6iF7bC4hY/ImJKN4REQILcSU9VKcKL8gfELZM8mM5BnLH1BsCc8kBdnVGD1LIFOs4W3O2idNHhON1r0w==", "license": "MIT", + "peer": true, "dependencies": { "@urql/core": "^5.1.1", "wonka": "^6.3.2" @@ -13108,6 +13091,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz", "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -13898,6 +13882,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, diff --git a/frontend/src/external/bcanSatchel/actions.ts b/frontend/src/external/bcanSatchel/actions.ts index 161e02a7..f00a2228 100644 --- a/frontend/src/external/bcanSatchel/actions.ts +++ b/frontend/src/external/bcanSatchel/actions.ts @@ -35,6 +35,10 @@ export const updateUserProfile = action("updateUserProfile", (user: User) => ({ * Completely log out the user (clear tokens, user data, etc.). */ export const logoutUser = action("logoutUser"); +/** + * Removes profile pic from the store and active users + */ +export const removeProfilePic = action("removeProfilePic"); /** * Moves along the all grants that are fetched from back end to mutator. diff --git a/frontend/src/external/bcanSatchel/mutators.ts b/frontend/src/external/bcanSatchel/mutators.ts index 291469af..a839f99a 100644 --- a/frontend/src/external/bcanSatchel/mutators.ts +++ b/frontend/src/external/bcanSatchel/mutators.ts @@ -12,6 +12,7 @@ import { updateSort, updateUserQuery, updateUserSort, + removeProfilePic, } from './actions'; import { getAppStore, persistToSessionStorage } from './store'; import { setActiveUsers, setInactiveUsers } from './actions'; @@ -129,4 +130,22 @@ mutator(updateUserQuery, (actionMessage) => { mutator(updateUserSort, (actionMessage) => { const store = getAppStore(); store.userSort = actionMessage.sort; -}) \ No newline at end of file +}) + +mutator(removeProfilePic, () => { + const store = getAppStore(); + + if (!store.user) return; + + delete store.user.profilePicUrl; + + const activeUserIndex = store.activeUsers?.findIndex( + (u) => u.email === store.user!.email + ); + + if (activeUserIndex !== undefined && activeUserIndex !== -1) { + delete store.activeUsers[activeUserIndex].profilePicUrl; + } + + persistToSessionStorage(); +}); \ No newline at end of file diff --git a/frontend/src/main-page/settings/Settings.tsx b/frontend/src/main-page/settings/Settings.tsx index 58075190..fc176559 100644 --- a/frontend/src/main-page/settings/Settings.tsx +++ b/frontend/src/main-page/settings/Settings.tsx @@ -9,7 +9,8 @@ import ChangePasswordModal from "./ChangePasswordModal"; import ProfilePictureModal from "./ProfilePictureModal"; import { getAppStore } from "../../external/bcanSatchel/store"; import { ALLOWED_PROFILE_PIC_EXTENSIONS, MAX_PROFILE_PIC_SIZE_MB } from "./profilePictureConstants"; - +import { removeProfilePic } from "../../external/bcanSatchel/actions"; +import {api} from "../../api" const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; function Settings() { @@ -58,6 +59,26 @@ function Settings() { setPersonalInfoError(null); }; + const handleRemoveProfilePic = async () => { + const store = getAppStore() + if(!store.user!.profilePicUrl){ + return; + } + const email = store.user?.email + + const response = await api('/user/remove-pfp', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email}) + }); + + if(!response.ok){ + // TODO: Put a real design here + alert("There was an error removing the profile picture") + } + removeProfilePic() + } + return (

Settings

@@ -95,7 +116,7 @@ function Settings() { />
From 98227dc7b10c8a09e48482199d4b88e1ffd82c64 Mon Sep 17 00:00:00 2001 From: prooflesben Date: Sat, 14 Mar 2026 18:45:17 -0400 Subject: [PATCH 4/5] Merged main and updated updateUser profile to change the inactive and active users depending on where the user is --- frontend/src/external/bcanSatchel/mutators.ts | 25 +++++++++++++++++++ frontend/src/main-page/settings/Settings.tsx | 3 +-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/frontend/src/external/bcanSatchel/mutators.ts b/frontend/src/external/bcanSatchel/mutators.ts index a839f99a..02107912 100644 --- a/frontend/src/external/bcanSatchel/mutators.ts +++ b/frontend/src/external/bcanSatchel/mutators.ts @@ -55,10 +55,35 @@ mutator(setAuthState, (actionMessage) => { mutator(updateUserProfile, (actionMessage) => { const store = getAppStore(); if (store.user) { + // Capture old email before overwriting + const oldEmail = store.user.email; + store.user = { ...store.user, ...actionMessage.user, }; + + const activeUserIndex = store.activeUsers?.findIndex( + (u) => u.email === oldEmail + ); + + if (activeUserIndex !== undefined && activeUserIndex !== -1) { + store.activeUsers[activeUserIndex] = { + ...store.user + }; + } else { + // Find and update the matching user in inactiveUsers by old email + const inactiveUserIndex = store.inactiveUsers?.findIndex( + (u) => u.email === oldEmail + ); + + if (inactiveUserIndex !== undefined && inactiveUserIndex !== -1) { + store.inactiveUsers[inactiveUserIndex] = { + ...store.user + }; + } + } + persistToSessionStorage(); } }); diff --git a/frontend/src/main-page/settings/Settings.tsx b/frontend/src/main-page/settings/Settings.tsx index 3cc8ca61..109ac02c 100644 --- a/frontend/src/main-page/settings/Settings.tsx +++ b/frontend/src/main-page/settings/Settings.tsx @@ -10,14 +10,13 @@ import { ALLOWED_PROFILE_PIC_EXTENSIONS, MAX_PROFILE_PIC_SIZE_MB } from "./profi import { removeProfilePic } from "../../external/bcanSatchel/actions"; import {api} from "../../api" import ChangePasswordModal, { ChangePasswordFormValues } from "./ChangePasswordModal"; -import { api } from "../../api"; import { getAppStore } from "../../external/bcanSatchel/store"; import { setActiveUsers, updateUserProfile } from "../../external/bcanSatchel/actions"; import { User } from "../../../../middle-layer/types/User"; const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; -export default function Settings() { +function Settings() { const store = getAppStore(); const user = store.user; const [personalInfo, setPersonalInfo] = useState({ From 8ca63fe71cad3b6459c8f4fbc383b504b9ae271f Mon Sep 17 00:00:00 2001 From: prooflesben Date: Sat, 14 Mar 2026 18:48:32 -0400 Subject: [PATCH 5/5] fixed the test --- backend/src/user/__test__/user.service.spec.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/src/user/__test__/user.service.spec.ts b/backend/src/user/__test__/user.service.spec.ts index 74411459..dd73b17e 100644 --- a/backend/src/user/__test__/user.service.spec.ts +++ b/backend/src/user/__test__/user.service.spec.ts @@ -156,7 +156,8 @@ describe('UserController', () => { const result = await userService.uploadProfilePic(user, file); - expect(result).toBe(s3Url); + expect(result).toContain(s3Url); + expect(result).toMatch(/\?t=\d+$/); expect(mockS3Upload).toHaveBeenCalledWith({ Bucket: 'test-profile-pics-bucket', Key: 'Emp-One-emp-profilepic.jpg', @@ -223,7 +224,8 @@ describe('UserController', () => { .mockResolvedValueOnce({ Attributes: { ...user, profilePicUrl: s3Url } }); const result = await userService.uploadProfilePic(user, createMockFile({ mimetype })); - expect(result).toBe(s3Url); + expect(result).toContain(s3Url); + expect(result).toMatch(/\?t=\d+$/); } });