From e43a1926505b060baaf72fa07cb2596e348a7b6f Mon Sep 17 00:00:00 2001 From: Brok3Turtl3 Date: Wed, 27 Mar 2024 13:23:58 -0400 Subject: [PATCH 01/29] CHE-30 Updated profile model along with type file. Refactored profileController to work with new model and adjust front end display component --- .../components/ProfileThumb/ProfileThumb.tsx | 4 +- server/controllers/profileController.ts | 64 ++++++++++-- server/models/profileModel.ts | 99 ++++++++++++------- server/models/userModel.ts | 4 +- server/types/profile.ts | 72 +++++++++++--- 5 files changed, 182 insertions(+), 61 deletions(-) diff --git a/client/src/components/ProfileThumb/ProfileThumb.tsx b/client/src/components/ProfileThumb/ProfileThumb.tsx index 99d53c7..764514b 100644 --- a/client/src/components/ProfileThumb/ProfileThumb.tsx +++ b/client/src/components/ProfileThumb/ProfileThumb.tsx @@ -11,8 +11,8 @@ const ProfileThumb = ({ profile }: ProfileThumbProps): JSX.Element => { return (
-

{profile.firstName}

-

{profile.bio}

+

{profile.fullName}

+

{profile.personalBio}

); }; diff --git a/server/controllers/profileController.ts b/server/controllers/profileController.ts index 70e8928..9ee60e0 100644 --- a/server/controllers/profileController.ts +++ b/server/controllers/profileController.ts @@ -10,26 +10,72 @@ const createProfile = async ( res: Response, next: NextFunction ) => { - const { user, firstName, lastName, bio, job, socials } = req.body; + // Now expecting more complex data structure based on the new IProfile interface + const { + user, + fullName, + profilePhoto, + cohort, + graduationYear, + email, + linkedInProfile, + professionalSummary, + skills, + specializations, + careerInformation, + education, + projects, + personalBio, + testimonials, + socialMediaLinks, + availabilityForNetworking, + bootcampExperience, + achievementsAndCertifications, + volunteerWork, + eventParticipation, + gallery, + blogOrWriting, + } = req.body; try { - const profile: IProfile = await Profile.create({ + // Create the profile with all the provided data + const profile = await Profile.create({ user, - firstName, - lastName, - bio, - job, - socials, + fullName, + profilePhoto, + cohort, + graduationYear, + email, + linkedInProfile, + professionalSummary, + skills, + specializations, + careerInformation, + education, + projects, + personalBio, + testimonials, + socialMediaLinks, + availabilityForNetworking, + bootcampExperience, + achievementsAndCertifications, + volunteerWork, + eventParticipation, + gallery, + blogOrWriting, }); if (profile) { return res.status(201).json(profile); } } catch (error) { + console.error(error); return next({ - log: "Express error in createProfile Middleware", + log: "Express error handler caught exception in createProfile", status: 500, - message: { err: "An error occurred during profile creation" }, + message: { + err: "An error occurred during profile creation. Please try again.", + }, }); } }; diff --git a/server/models/profileModel.ts b/server/models/profileModel.ts index 308c38c..4b770d0 100644 --- a/server/models/profileModel.ts +++ b/server/models/profileModel.ts @@ -1,46 +1,75 @@ -import mongoose from "mongoose"; +// profileModel.ts +import mongoose, { Schema } from "mongoose"; import { IProfile } from "../types/profile"; -const profileSchema = new mongoose.Schema({ - user: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - required: true, - }, - bio: { - type: String, - }, - firstName: { - type: String, - }, - lastName: { - type: String, - }, - job: { - title: String, - company: String, - description: String, - date: Date, - }, - socials: { - linkedIn: { - type: String, - }, - github: { - type: String, +const profileSchema = new Schema({ + user: { type: Schema.Types.ObjectId, ref: "User", required: true }, + fullName: { type: String, required: true }, + profilePhoto: String, + cohort: String, + graduationYear: Number, + email: String, + linkedInProfile: String, + professionalSummary: String, + skills: [String], + specializations: [String], + careerInformation: { + currentPosition: { + title: String, + company: String, }, - twitter: { - type: String, + pastPositions: [ + { + title: String, + company: String, + startDate: Date, + endDate: Date, + }, + ], + }, + education: [ + { + institution: String, + degree: String, + fieldOfStudy: String, + startDate: Date, + endDate: Date, }, - facebook: { - type: String, + ], + projects: [ + { + name: String, + description: String, + link: String, }, - instagram: { - type: String, + ], + personalBio: String, + testimonials: [ + { + from: String, + relation: String, + text: String, }, + ], + socialMediaLinks: { + twitter: String, + blog: String, + other: [String], }, + availabilityForNetworking: Boolean, + bootcampExperience: String, + achievementsAndCertifications: [String], + volunteerWork: [String], + eventParticipation: [String], + gallery: [String], + blogOrWriting: [ + { + title: String, + link: String, + }, + ], }); -const Profile = mongoose.model("Profile", profileSchema); +const Profile = mongoose.model("profiles", profileSchema); export default Profile; diff --git a/server/models/userModel.ts b/server/models/userModel.ts index ffcfcf4..f5aaadd 100644 --- a/server/models/userModel.ts +++ b/server/models/userModel.ts @@ -1,8 +1,8 @@ -import mongoose, { Document } from "mongoose"; +import mongoose, { Schema } from "mongoose"; import bcrypt from "bcryptjs"; import { IUser } from "../types/user"; -const userSchema = new mongoose.Schema({ +const userSchema = new Schema({ firstName: { type: String, required: true, diff --git a/server/types/profile.ts b/server/types/profile.ts index b0e2056..f7dd63e 100644 --- a/server/types/profile.ts +++ b/server/types/profile.ts @@ -1,25 +1,71 @@ import { Document, ObjectId } from "mongoose"; -interface ISocial { - linkedIn?: string; - github?: string; +interface ISocialLinks { twitter?: string; - facebook?: string; - instagram?: string; + blog?: string; + other?: string[]; } -interface IJob { +interface IProject { + name: string; + description?: string; + link?: string; +} + +interface ICareerPosition { title?: string; company?: string; - description?: string; - date?: Date; + startDate?: Date; + endDate?: Date; +} + +interface IEducation { + institution: string; + degree?: string; + fieldOfStudy?: string; + startDate?: Date; + endDate?: Date; +} + +interface ITestimonial { + from: string; + relation?: string; + text: string; +} + +interface IBlogOrWriting { + title: string; + link: string; } export interface IProfile extends Document { user: ObjectId; - firstName: String; - lastName: String; - bio?: string; - job?: IJob; - socials?: ISocial; + fullName: string; + profilePhoto?: string; + cohort?: string; + graduationYear?: number; + email?: string; + linkedInProfile?: string; + professionalSummary?: string; + skills?: string[]; + specializations?: string[]; + careerInformation?: { + currentPosition?: { + title?: string; + company?: string; + }; + pastPositions?: ICareerPosition[]; + }; + education?: IEducation[]; + projects?: IProject[]; + personalBio?: string; + testimonials?: ITestimonial[]; + socialMediaLinks?: ISocialLinks; + availabilityForNetworking?: boolean; + bootcampExperience?: string; + achievementsAndCertifications?: string[]; + volunteerWork?: string[]; + eventParticipation?: string[]; + gallery?: string[]; + blogOrWriting?: IBlogOrWriting[]; } From 57224d35da55e44717e31f1986e944d939018fe3 Mon Sep 17 00:00:00 2001 From: Brok3Turtl3 Date: Wed, 27 Mar 2024 13:26:48 -0400 Subject: [PATCH 02/29] CHE-30 clean up --- server/controllers/profileController.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/controllers/profileController.ts b/server/controllers/profileController.ts index 9ee60e0..444ba71 100644 --- a/server/controllers/profileController.ts +++ b/server/controllers/profileController.ts @@ -10,7 +10,6 @@ const createProfile = async ( res: Response, next: NextFunction ) => { - // Now expecting more complex data structure based on the new IProfile interface const { user, fullName, @@ -38,7 +37,6 @@ const createProfile = async ( } = req.body; try { - // Create the profile with all the provided data const profile = await Profile.create({ user, fullName, From b6a5cd2f09b0c52a71de178902e01f9768ec55cf Mon Sep 17 00:00:00 2001 From: Brok3Turtl3 Date: Wed, 27 Mar 2024 13:28:08 -0400 Subject: [PATCH 03/29] CHE-30 clean up --- server/controllers/profileController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/controllers/profileController.ts b/server/controllers/profileController.ts index 444ba71..81ace0c 100644 --- a/server/controllers/profileController.ts +++ b/server/controllers/profileController.ts @@ -69,7 +69,7 @@ const createProfile = async ( } catch (error) { console.error(error); return next({ - log: "Express error handler caught exception in createProfile", + log: "Express error in createProfile Middleware", status: 500, message: { err: "An error occurred during profile creation. Please try again.", From 088005be04a3937a9b542ffa203dc656b6c90989 Mon Sep 17 00:00:00 2001 From: Brok3Turtl3 Date: Thu, 28 Mar 2024 08:33:06 -0400 Subject: [PATCH 04/29] CHE-30 moved profile controller tests to refactor folder for future work --- {__tests__ => __refactor_tests__}/profileController.test.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {__tests__ => __refactor_tests__}/profileController.test.ts (100%) diff --git a/__tests__/profileController.test.ts b/__refactor_tests__/profileController.test.ts similarity index 100% rename from __tests__/profileController.test.ts rename to __refactor_tests__/profileController.test.ts From cbc00325ec5cc7ebbdb579a69693c01c3954373b Mon Sep 17 00:00:00 2001 From: Brok3Turtl3 Date: Thu, 28 Mar 2024 08:38:02 -0400 Subject: [PATCH 05/29] CHE-30 Updated profile type file --- client/types/profile.ts | 78 +++++++++++++++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 15 deletions(-) diff --git a/client/types/profile.ts b/client/types/profile.ts index 6911e62..f7dd63e 100644 --- a/client/types/profile.ts +++ b/client/types/profile.ts @@ -1,23 +1,71 @@ -interface ISocial { - linkedIn?: string; - github?: string; +import { Document, ObjectId } from "mongoose"; + +interface ISocialLinks { twitter?: string; - facebook?: string; - instagram?: string; + blog?: string; + other?: string[]; +} + +interface IProject { + name: string; + description?: string; + link?: string; } -interface IJob { +interface ICareerPosition { title?: string; company?: string; - description?: string; - date?: Date; + startDate?: Date; + endDate?: Date; +} + +interface IEducation { + institution: string; + degree?: string; + fieldOfStudy?: string; + startDate?: Date; + endDate?: Date; +} + +interface ITestimonial { + from: string; + relation?: string; + text: string; +} + +interface IBlogOrWriting { + title: string; + link: string; } -export interface IProfile { - user: string; - firstName: string; - lastName: string; - bio?: string; - job?: IJob; - socials?: ISocial; +export interface IProfile extends Document { + user: ObjectId; + fullName: string; + profilePhoto?: string; + cohort?: string; + graduationYear?: number; + email?: string; + linkedInProfile?: string; + professionalSummary?: string; + skills?: string[]; + specializations?: string[]; + careerInformation?: { + currentPosition?: { + title?: string; + company?: string; + }; + pastPositions?: ICareerPosition[]; + }; + education?: IEducation[]; + projects?: IProject[]; + personalBio?: string; + testimonials?: ITestimonial[]; + socialMediaLinks?: ISocialLinks; + availabilityForNetworking?: boolean; + bootcampExperience?: string; + achievementsAndCertifications?: string[]; + volunteerWork?: string[]; + eventParticipation?: string[]; + gallery?: string[]; + blogOrWriting?: IBlogOrWriting[]; } From c307153d03b592c8461688279055d22db68a7f37 Mon Sep 17 00:00:00 2001 From: Brok3Turtl3 Date: Thu, 28 Mar 2024 08:48:11 -0400 Subject: [PATCH 06/29] CHE-30 Adjusted code on Profile page --- client/src/pages/Profile/Profile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/pages/Profile/Profile.tsx b/client/src/pages/Profile/Profile.tsx index 3a0cbed..289b7c5 100644 --- a/client/src/pages/Profile/Profile.tsx +++ b/client/src/pages/Profile/Profile.tsx @@ -14,7 +14,7 @@ const Profile = (): JSX.Element => {

Profile

{user?._id}

-

{userProfile?.bio}

+

{userProfile?.fullName}

); }; From 554608e9a0a1a1907cf9812ea25d3e5f9f6a5ee8 Mon Sep 17 00:00:00 2001 From: Brok3Turtl3 Date: Thu, 28 Mar 2024 08:53:48 -0400 Subject: [PATCH 07/29] CHE-30 Adjusted key value to use the elements id property --- client/src/pages/Profiles/Profiles.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/pages/Profiles/Profiles.tsx b/client/src/pages/Profiles/Profiles.tsx index 1aed94f..916dae4 100644 --- a/client/src/pages/Profiles/Profiles.tsx +++ b/client/src/pages/Profiles/Profiles.tsx @@ -18,7 +18,7 @@ const Profiles = (): JSX.Element => {
{profiles.map((profile) => ( - + ))}
From 01223cc4928c93910cf9ca5c1d23cf8791444a4d Mon Sep 17 00:00:00 2001 From: Brok3Turtl3 Date: Thu, 28 Mar 2024 09:44:32 -0400 Subject: [PATCH 08/29] CHE-7 Seting up basic component --- client/src/pages/EditProfilePage/EditProfilePage.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 client/src/pages/EditProfilePage/EditProfilePage.tsx diff --git a/client/src/pages/EditProfilePage/EditProfilePage.tsx b/client/src/pages/EditProfilePage/EditProfilePage.tsx new file mode 100644 index 0000000..19fdf8d --- /dev/null +++ b/client/src/pages/EditProfilePage/EditProfilePage.tsx @@ -0,0 +1,12 @@ +import React, { useEffect, useState } from "react"; +import { useAppSelector, useAppDispatch } from "../../app/hooks"; +import { + fetchUserProfile, + updateUserProfile, +} from "../../features/userProfile/userProfileSlice"; + +const EditProfilePage = () => { + const dispatch = useAppDispatch(); +}; + +export default EditProfilePage; From a1f966b796ef89bb056b44cac441ac6abb43c687 Mon Sep 17 00:00:00 2001 From: Brok3Turtl3 Date: Thu, 28 Mar 2024 10:12:26 -0400 Subject: [PATCH 09/29] CHE-7 Basic edit page set-up --- client/src/AuthenticatedApp.tsx | 2 + client/src/components/Banner/Banner.tsx | 8 +- .../pages/EditProfilePage/EditProfilePage.tsx | 82 ++++++++++++++++++- server/models/profileModel.ts | 1 - 4 files changed, 87 insertions(+), 6 deletions(-) diff --git a/client/src/AuthenticatedApp.tsx b/client/src/AuthenticatedApp.tsx index fd71270..559989a 100644 --- a/client/src/AuthenticatedApp.tsx +++ b/client/src/AuthenticatedApp.tsx @@ -7,6 +7,7 @@ import MainPage from "./pages/MainPage/MainPage"; import Forums from "./pages/Forums/Forums"; import Profiles from "./pages/Profiles/Profiles"; import Profile from "./pages/Profile/Profile"; +import EditProfilePage from "./pages/EditProfilePage/EditProfilePage"; import NotFoundPage from "./pages/NotFoundPage/NotFoundPage"; import { useNavigate } from "react-router-dom"; @@ -27,6 +28,7 @@ const AuthenticatedApp = () => { } /> } /> } /> + } /> } /> } /> diff --git a/client/src/components/Banner/Banner.tsx b/client/src/components/Banner/Banner.tsx index 26da12c..3729191 100644 --- a/client/src/components/Banner/Banner.tsx +++ b/client/src/components/Banner/Banner.tsx @@ -16,8 +16,8 @@ const Banner = (): JSX.Element => { //TODO CLEAR ALL STATE }; - const goToProfile = () => { - navigate("profile"); + const goToEditProfile = () => { + navigate("editProfile"); setShowDropdown(false); }; return ( @@ -40,9 +40,9 @@ const Banner = (): JSX.Element => { - Go to Profile + Edit Profile { const dispatch = useAppDispatch(); + const { profile, status } = useAppSelector((state) => state.userProfile); + const userID = useAppSelector((state) => state.user.userData._id); + + const [formData, setFormData] = useState({ + fullName: "", + email: "", + personalBio: "", + }); + + useEffect(() => { + dispatch(fetchUserProfile(userID)); + }, [dispatch]); + + useEffect(() => { + if (profile) { + setFormData({ + fullName: profile.fullName || "", + email: profile.email || "", + }); + } + }, [profile]); + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData((prevFormData) => ({ + ...prevFormData, + [name]: value, + })); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + // Again, assuming you have a way to include the user ID + const userID = "currentUser_ID"; + dispatch(updateUserProfile({ ...formData, userID })); + }; + + if (status === "loading") { + return
Loading...
; + } + + return ( +
+

Edit Profile

+
+
+ + +
+
+ + +
+
+ + +
+ + +
+
+ ); }; export default EditProfilePage; diff --git a/server/models/profileModel.ts b/server/models/profileModel.ts index 4b770d0..bfdd800 100644 --- a/server/models/profileModel.ts +++ b/server/models/profileModel.ts @@ -1,4 +1,3 @@ -// profileModel.ts import mongoose, { Schema } from "mongoose"; import { IProfile } from "../types/profile"; From a6d7d5d11418a8a34cacafbcb434610d772a909a Mon Sep 17 00:00:00 2001 From: Brok3Turtl3 Date: Thu, 28 Mar 2024 11:09:26 -0400 Subject: [PATCH 10/29] CHE-7 Updated userProfileSlice to include new action creator and state functionality for updating users profile --- .../features/userProfile/userProfileSlice.ts | 32 ++++++++++++++++++- .../pages/EditProfilePage/EditProfilePage.tsx | 2 +- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/client/src/features/userProfile/userProfileSlice.ts b/client/src/features/userProfile/userProfileSlice.ts index 0d67961..9c4f4b7 100644 --- a/client/src/features/userProfile/userProfileSlice.ts +++ b/client/src/features/userProfile/userProfileSlice.ts @@ -4,7 +4,7 @@ import { IProfile } from "../../../types/profile"; export interface ProfileState { profile: IProfile | null; - status: "idle" | "loading" | "failed"; + status: "idle" | "loading" | "failed" | "updating"; error: string | null; } @@ -31,6 +31,25 @@ export const fetchUserProfile = createAsyncThunk( } ); +export const updateUserProfile = createAsyncThunk( + "profile/updateUserProfile", + async ( + { userID, ...updateData }: Partial & { userID: string }, + thunkAPI + ) => { + try { + const response = await axios.put(`/api/profiles/${userID}`, updateData); + return response.data; + } catch (error) { + let errorMessage = "An error occurred during profile update"; + if (axios.isAxiosError(error)) { + errorMessage = error.response?.data || errorMessage; + } + return thunkAPI.rejectWithValue(errorMessage); + } + } +); + const userProfileSlice = createSlice({ name: "profile", initialState, @@ -53,6 +72,17 @@ const userProfileSlice = createSlice({ .addCase(fetchUserProfile.rejected, (state, action) => { state.status = "failed"; state.error = action.payload as string; + }) + .addCase(updateUserProfile.pending, (state) => { + state.status = "updating"; + }) + .addCase(updateUserProfile.fulfilled, (state, action) => { + state.profile = action.payload; // Assuming the backend returns the updated profile + state.status = "idle"; + }) + .addCase(updateUserProfile.rejected, (state, action) => { + state.status = "failed"; + state.error = action.payload as string; }); }, }); diff --git a/client/src/pages/EditProfilePage/EditProfilePage.tsx b/client/src/pages/EditProfilePage/EditProfilePage.tsx index e5e4399..b5b59d0 100644 --- a/client/src/pages/EditProfilePage/EditProfilePage.tsx +++ b/client/src/pages/EditProfilePage/EditProfilePage.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import { useAppSelector, useAppDispatch } from "../../app/hooks"; import { fetchUserProfile, - // updateUserProfile, + updateUserProfile, } from "../../features/userProfile/userProfileSlice"; const EditProfilePage = () => { From bbf064db4b9add53789f49a570311da50a9b1100 Mon Sep 17 00:00:00 2001 From: Brok3Turtl3 Date: Thu, 28 Mar 2024 14:42:07 -0400 Subject: [PATCH 11/29] CHE-7 Basic functionality in place for edits and state updates --- .../src/pages/EditProfilePage/EditProfilePage.tsx | 13 ++++++++----- server/controllers/profileController.ts | 12 +++++------- server/routes/profileRoutes.ts | 2 +- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/client/src/pages/EditProfilePage/EditProfilePage.tsx b/client/src/pages/EditProfilePage/EditProfilePage.tsx index b5b59d0..2ff77fa 100644 --- a/client/src/pages/EditProfilePage/EditProfilePage.tsx +++ b/client/src/pages/EditProfilePage/EditProfilePage.tsx @@ -8,7 +8,7 @@ import { const EditProfilePage = () => { const dispatch = useAppDispatch(); const { profile, status } = useAppSelector((state) => state.userProfile); - const userID = useAppSelector((state) => state.user.userData._id); + const userID = useAppSelector((state) => state.user.userData?._id); const [formData, setFormData] = useState({ fullName: "", @@ -17,7 +17,7 @@ const EditProfilePage = () => { }); useEffect(() => { - dispatch(fetchUserProfile(userID)); + if (userID) dispatch(fetchUserProfile(userID as string)); }, [dispatch]); useEffect(() => { @@ -25,6 +25,7 @@ const EditProfilePage = () => { setFormData({ fullName: profile.fullName || "", email: profile.email || "", + personalBio: profile.personalBio || "", }); } }, [profile]); @@ -39,12 +40,14 @@ const EditProfilePage = () => { const handleSubmit = (e) => { e.preventDefault(); - // Again, assuming you have a way to include the user ID - const userID = "currentUser_ID"; + if (!userID) { + console.error("UserID is undefined."); + return; + } dispatch(updateUserProfile({ ...formData, userID })); }; - if (status === "loading") { + if (status === "loading" || !userID) { return
Loading...
; } diff --git a/server/controllers/profileController.ts b/server/controllers/profileController.ts index 81ace0c..af31c69 100644 --- a/server/controllers/profileController.ts +++ b/server/controllers/profileController.ts @@ -78,7 +78,7 @@ const createProfile = async ( } }; -// ENDPOINT PATCH api/profiles/:UserID +// ENDPOINT PUT api/profiles/:UserID // PURPOSE Update an existing profile // ACCESS Private const updateProfile = async ( @@ -87,13 +87,11 @@ const updateProfile = async ( next: NextFunction ) => { const { userID } = req.params; - const { firstName, lastName, bio, job, socials } = req.body; + const { fullName, email, personalBio } = req.body; const newProfile = { - firstName, - lastName, - bio, - job, - socials, + fullName, + email, + personalBio, }; try { diff --git a/server/routes/profileRoutes.ts b/server/routes/profileRoutes.ts index 5b843d2..802c050 100644 --- a/server/routes/profileRoutes.ts +++ b/server/routes/profileRoutes.ts @@ -10,7 +10,7 @@ import { const router = express.Router(); router.post("/", createProfile); -router.patch("/:userID", updateProfile); +router.put("/:userID", updateProfile); router.get("/:userID", getProfileById); router.get("/", getAllProfiles); From 09877fd359403b6c82e20d0611468def77fc2cdc Mon Sep 17 00:00:00 2001 From: Brok3Turtl3 Date: Fri, 29 Mar 2024 08:00:18 -0400 Subject: [PATCH 12/29] CHE-7 Updated test to reflect new button text and added event typing for handlers --- client/src/components/Banner/Banner.test.tsx | 2 +- client/src/pages/EditProfilePage/EditProfilePage.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/components/Banner/Banner.test.tsx b/client/src/components/Banner/Banner.test.tsx index f2ed51a..da3da40 100644 --- a/client/src/components/Banner/Banner.test.tsx +++ b/client/src/components/Banner/Banner.test.tsx @@ -62,7 +62,7 @@ describe("Banner Component", () => { const optionsButton = screen.getByRole("button", { name: "Options" }); fireEvent.click(optionsButton); - const profileOption = screen.getByText("Go to Profile"); + const profileOption = screen.getByText("Edit Profile"); const logoutOption = screen.getByText("Logout"); expect(profileOption).toBeInTheDocument(); diff --git a/client/src/pages/EditProfilePage/EditProfilePage.tsx b/client/src/pages/EditProfilePage/EditProfilePage.tsx index 2ff77fa..aee251a 100644 --- a/client/src/pages/EditProfilePage/EditProfilePage.tsx +++ b/client/src/pages/EditProfilePage/EditProfilePage.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, ChangeEvent, FormEvent } from "react"; import { useAppSelector, useAppDispatch } from "../../app/hooks"; import { fetchUserProfile, @@ -30,7 +30,7 @@ const EditProfilePage = () => { } }, [profile]); - const handleChange = (e) => { + const handleChange = (e: ChangeEvent) => { const { name, value } = e.target; setFormData((prevFormData) => ({ ...prevFormData, @@ -38,7 +38,7 @@ const EditProfilePage = () => { })); }; - const handleSubmit = (e) => { + const handleSubmit = (e: FormEvent) => { e.preventDefault(); if (!userID) { console.error("UserID is undefined."); From 3d4a5e1468f4480ed035528e0da3f7f9c8efb7bf Mon Sep 17 00:00:00 2001 From: Brok3Turtl3 Date: Fri, 29 Mar 2024 08:07:35 -0400 Subject: [PATCH 13/29] CHE-7 Updated Banner button test for editProfile routing --- client/src/components/Banner/Banner.test.tsx | 4 ++-- client/src/pages/Profiles/Profiles.tsx | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/client/src/components/Banner/Banner.test.tsx b/client/src/components/Banner/Banner.test.tsx index da3da40..7d7e4dd 100644 --- a/client/src/components/Banner/Banner.test.tsx +++ b/client/src/components/Banner/Banner.test.tsx @@ -74,10 +74,10 @@ describe("Banner Component", () => { const optionsButton = screen.getByRole("button", { name: "Options" }); fireEvent.click(optionsButton); - const profileOption = screen.getByText("Go to Profile"); + const profileOption = screen.getByText("Edit Profile"); fireEvent.click(profileOption); - expect(mockNavigate).toHaveBeenCalledWith("profile"); + expect(mockNavigate).toHaveBeenCalledWith("editProfile"); }); it("handles logout on clicking Logout", () => { diff --git a/client/src/pages/Profiles/Profiles.tsx b/client/src/pages/Profiles/Profiles.tsx index 916dae4..9795e37 100644 --- a/client/src/pages/Profiles/Profiles.tsx +++ b/client/src/pages/Profiles/Profiles.tsx @@ -17,8 +17,9 @@ const Profiles = (): JSX.Element => {

PROFILES

- {profiles.map((profile) => ( - + {/* TODO Look at better key for this */} + {profiles.map((profile, index) => ( + ))}
From b57b95a4e14e4f77e2af2a26ca37761ef9e00d3f Mon Sep 17 00:00:00 2001 From: Brok3Turtl3 Date: Fri, 29 Mar 2024 08:29:38 -0400 Subject: [PATCH 14/29] CHE-47 Basic styling added --- .../pages/EditProfilePage/EditProfilePage.tsx | 103 ++++++++++++------ 1 file changed, 67 insertions(+), 36 deletions(-) diff --git a/client/src/pages/EditProfilePage/EditProfilePage.tsx b/client/src/pages/EditProfilePage/EditProfilePage.tsx index aee251a..4e9bad4 100644 --- a/client/src/pages/EditProfilePage/EditProfilePage.tsx +++ b/client/src/pages/EditProfilePage/EditProfilePage.tsx @@ -52,42 +52,73 @@ const EditProfilePage = () => { } return ( -
-

Edit Profile

-
-
- - -
-
- - -
-
- - -
- - -
+
+
+

+ Edit Profile +

+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
); }; From 45ecea3ec96782b5e127b545719b2b043bdeb9e1 Mon Sep 17 00:00:00 2001 From: Brok3Turtl3 Date: Sat, 30 Mar 2024 11:27:52 -0400 Subject: [PATCH 15/29] CHE-50 Started working on imageController --- server/controllers/imageController.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 server/controllers/imageController.ts diff --git a/server/controllers/imageController.ts b/server/controllers/imageController.ts new file mode 100644 index 0000000..57fbe0b --- /dev/null +++ b/server/controllers/imageController.ts @@ -0,0 +1,10 @@ +import { Request, Response } from "express"; +import AWS from "aws-sdk"; + +AWS.config.update({ + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + region: process.env.AWS_REGION, +}); + +const s3 = new AWS.S3(); From 20279106e90a5c77c056cf24a03e5985a9cf4703 Mon Sep 17 00:00:00 2001 From: Brok3Turtl3 Date: Mon, 1 Apr 2024 08:16:38 -0400 Subject: [PATCH 16/29] CHE-50 Front and back ends set up for image upload and retrieval from S3 for EditProfile page --- .../features/userProfile/userProfileSlice.ts | 40 +- .../pages/EditProfilePage/EditProfilePage.tsx | 41 ++ package-lock.json | 527 ++++++++++++++++-- package.json | 3 + server/controllers/imageController.ts | 68 ++- server/controllers/profileController.ts | 22 +- server/index.ts | 2 + server/routes/imageRoutes.ts | 23 + 8 files changed, 681 insertions(+), 45 deletions(-) create mode 100644 server/routes/imageRoutes.ts diff --git a/client/src/features/userProfile/userProfileSlice.ts b/client/src/features/userProfile/userProfileSlice.ts index 9c4f4b7..57f4543 100644 --- a/client/src/features/userProfile/userProfileSlice.ts +++ b/client/src/features/userProfile/userProfileSlice.ts @@ -50,6 +50,33 @@ export const updateUserProfile = createAsyncThunk( } ); +export const uploadProfilePicture = createAsyncThunk( + "profile/uploadProfilePicture", + async ( + { formData, userID }: { formData: FormData; userID: string }, + thunkAPI + ) => { + try { + const response = await axios.post( + `/api/images/profile-picture/${userID}`, + formData, + { + headers: { + "Content-Type": "multipart/form-data", + }, + } + ); + return response.data; + } catch (error) { + let errorMessage = "An error occurred during profile picture upload"; + if (axios.isAxiosError(error)) { + errorMessage = error.response?.data || errorMessage; + } + return thunkAPI.rejectWithValue(errorMessage); + } + } +); + const userProfileSlice = createSlice({ name: "profile", initialState, @@ -77,12 +104,23 @@ const userProfileSlice = createSlice({ state.status = "updating"; }) .addCase(updateUserProfile.fulfilled, (state, action) => { - state.profile = action.payload; // Assuming the backend returns the updated profile + state.profile = action.payload; state.status = "idle"; }) .addCase(updateUserProfile.rejected, (state, action) => { state.status = "failed"; state.error = action.payload as string; + }) + .addCase(uploadProfilePicture.pending, (state) => { + state.status = "updating"; + }) + .addCase(uploadProfilePicture.fulfilled, (state, action) => { + state.profile = action.payload; + state.status = "idle"; + }) + .addCase(uploadProfilePicture.rejected, (state, action) => { + state.status = "failed"; + state.error = action.payload as string; }); }, }); diff --git a/client/src/pages/EditProfilePage/EditProfilePage.tsx b/client/src/pages/EditProfilePage/EditProfilePage.tsx index 4e9bad4..fb51444 100644 --- a/client/src/pages/EditProfilePage/EditProfilePage.tsx +++ b/client/src/pages/EditProfilePage/EditProfilePage.tsx @@ -3,6 +3,7 @@ import { useAppSelector, useAppDispatch } from "../../app/hooks"; import { fetchUserProfile, updateUserProfile, + uploadProfilePicture, } from "../../features/userProfile/userProfileSlice"; const EditProfilePage = () => { @@ -16,6 +17,8 @@ const EditProfilePage = () => { personalBio: "", }); + const [file, setFile] = useState(null); + useEffect(() => { if (userID) dispatch(fetchUserProfile(userID as string)); }, [dispatch]); @@ -38,6 +41,12 @@ const EditProfilePage = () => { })); }; + const handleFileChange = (e: ChangeEvent) => { + const fileList = e.target.files; + if (!fileList) return; + setFile(fileList[0]); + }; + const handleSubmit = (e: FormEvent) => { e.preventDefault(); if (!userID) { @@ -47,6 +56,18 @@ const EditProfilePage = () => { dispatch(updateUserProfile({ ...formData, userID })); }; + const handleImageUpload = () => { + if (!file || !userID) { + console.error("File or UserID is undefined."); + return; + } + + const formData = new FormData(); + formData.append("profilePicture", file); + + dispatch(uploadProfilePicture({ formData, userID })); + }; + if (status === "loading" || !userID) { return
Loading...
; } @@ -54,6 +75,15 @@ const EditProfilePage = () => { return (
+ {profile?.profilePhoto && ( +
+ Profile +
+ )}

Edit Profile

@@ -109,6 +139,7 @@ const EditProfilePage = () => { onChange={handleChange} />
+
+
+

Upload Profile Picture

+ + +
); diff --git a/package-lock.json b/package-lock.json index 38eb51a..718bf55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "aws-sdk": "^2.1589.0", "bcryptjs": "^2.4.3", "cookie-parser": "^1.4.6", "dotenv": "^16.3.1", @@ -17,6 +18,7 @@ "html-webpack-plugin": "^5.5.3", "jsonwebtoken": "^9.0.1", "mongoose": "^7.3.4", + "multer": "^1.4.5-lts.1", "react": "^18.2.0", "webpack": "^5.88.1" }, @@ -30,6 +32,7 @@ "@types/jest": "^29.5.3", "@types/jsonwebtoken": "^9.0.2", "@types/mongoose": "^5.11.97", + "@types/multer": "^1.4.11", "@types/node": "^20.5.1", "@types/react": "^18.2.39", "@types/react-test-renderer": "^18.0.7", @@ -2896,6 +2899,15 @@ "mongoose": "*" } }, + "node_modules/@types/multer": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.11.tgz", + "integrity": "sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "20.5.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.1.tgz", @@ -3503,6 +3515,11 @@ "node": ">= 8" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -3561,7 +3578,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -3569,6 +3585,43 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws-sdk": { + "version": "2.1589.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1589.0.tgz", + "integrity": "sha512-Tt3UHH6hoUEAjbCscqvfEAoq9VSTN5iSQO9XSisiiH/QJo8sf+iLCYmfJHM4tVkd92bQH61/xxj9t2Mazwc/WQ==", + "hasInstallScript": true, + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-sdk/node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/aws-sdk/node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/axios": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", @@ -3716,6 +3769,25 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -3864,6 +3936,16 @@ "node": ">=14.20.1" } }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -3874,6 +3956,17 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -4190,6 +4283,47 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/concat-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/concurrently": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.0.tgz", @@ -4306,8 +4440,7 @@ "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, "node_modules/create-require": { "version": "1.1.1", @@ -5208,7 +5341,6 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, "dependencies": { "is-callable": "^1.1.3" } @@ -5488,7 +5620,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, "dependencies": { "has-symbols": "^1.0.2" }, @@ -5836,6 +5967,11 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -5944,7 +6080,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -6020,7 +6155,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -6097,6 +6231,20 @@ "node": ">=6" } }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -6255,7 +6403,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", - "dev": true, "dependencies": { "which-typed-array": "^1.1.11" }, @@ -6303,8 +6450,7 @@ "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "node_modules/isexe": { "version": "2.0.0", @@ -7081,6 +7227,14 @@ "node": ">= 10.13.0" } }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -7507,7 +7661,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7636,6 +7789,34 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/multer": { + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/multer/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/multicast-dns": { "version": "7.2.5", "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", @@ -7818,7 +7999,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -8172,8 +8352,7 @@ "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "node_modules/prompts": { "version": "2.4.2", @@ -8256,6 +8435,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -8753,6 +8941,11 @@ "node": ">=6" } }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -9250,6 +9443,14 @@ "node": ">= 0.4" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -9805,6 +10006,11 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "node_modules/typescript": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", @@ -9918,6 +10124,15 @@ "punycode": "^2.1.0" } }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, "node_modules/url-parse": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", @@ -9928,6 +10143,11 @@ "requires-port": "^1.0.0" } }, + "node_modules/url/node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + }, "node_modules/use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", @@ -9937,11 +10157,22 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/utila": { "version": "0.4.0", @@ -10479,7 +10710,6 @@ "version": "1.1.13", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", - "dev": true, "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.4", @@ -10566,6 +10796,26 @@ "node": ">=12" } }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", @@ -10576,7 +10826,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, "engines": { "node": ">=0.4" } @@ -12775,6 +13024,15 @@ "mongoose": "*" } }, + "@types/multer": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.11.tgz", + "integrity": "sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, "@types/node": { "version": "20.5.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.1.tgz", @@ -13295,6 +13553,11 @@ "picomatch": "^2.0.4" } }, + "append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, "arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -13349,8 +13612,36 @@ "available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "dev": true + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==" + }, + "aws-sdk": { + "version": "2.1589.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1589.0.tgz", + "integrity": "sha512-Tt3UHH6hoUEAjbCscqvfEAoq9VSTN5iSQO9XSisiiH/QJo8sf+iLCYmfJHM4tVkd92bQH61/xxj9t2Mazwc/WQ==", + "requires": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.6.2" + }, + "dependencies": { + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==" + }, + "uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==" + } + } }, "axios": { "version": "1.6.2", @@ -13469,6 +13760,11 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, "batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -13583,6 +13879,16 @@ "resolved": "https://registry.npmjs.org/bson/-/bson-5.4.0.tgz", "integrity": "sha512-WRZ5SQI5GfUuKnPTNmAYPiKIof3ORXAF4IRU5UcgmivNIon01rWQlw5RUH954dpu8yGL8T59YShVddIPaU/gFA==" }, + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -13593,6 +13899,14 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "requires": { + "streamsearch": "^1.1.0" + } + }, "bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -13821,6 +14135,46 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "concurrently": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.0.tgz", @@ -13907,8 +14261,7 @@ "core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, "create-require": { "version": "1.1.1", @@ -14596,7 +14949,6 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, "requires": { "is-callable": "^1.1.3" } @@ -14794,7 +15146,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, "requires": { "has-symbols": "^1.0.2" } @@ -15054,6 +15405,11 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, "ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -15134,7 +15490,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, "requires": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -15188,8 +15543,7 @@ "is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" }, "is-core-module": { "version": "2.12.1", @@ -15233,6 +15587,14 @@ "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", "dev": true }, + "is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, "is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -15337,7 +15699,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", - "dev": true, "requires": { "which-typed-array": "^1.1.11" } @@ -15370,8 +15731,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "isexe": { "version": "2.0.0", @@ -15970,6 +16330,11 @@ "supports-color": "^8.0.0" } }, + "jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -16301,8 +16666,7 @@ "minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" }, "mkdirp": { "version": "1.0.4", @@ -16384,6 +16748,30 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "multer": { + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "requires": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "requires": { + "minimist": "^1.2.6" + } + } + } + }, "multicast-dns": { "version": "7.2.5", "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", @@ -16527,8 +16915,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "object-inspect": { "version": "1.12.3", @@ -16784,8 +17171,7 @@ "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "prompts": { "version": "2.4.2", @@ -16843,6 +17229,11 @@ "side-channel": "^1.0.4" } }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==" + }, "querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -17204,6 +17595,11 @@ "sparse-bitfield": "^3.0.3" } }, + "sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" + }, "saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -17605,6 +18001,11 @@ "internal-slot": "^1.0.4" } }, + "streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" + }, "string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -17972,6 +18373,11 @@ "mime-types": "~2.1.24" } }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "typescript": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", @@ -18040,6 +18446,22 @@ "punycode": "^2.1.0" } }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + } + } + }, "url-parse": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", @@ -18057,11 +18479,22 @@ "dev": true, "requires": {} }, + "util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "requires": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "utila": { "version": "0.4.0", @@ -18446,7 +18879,6 @@ "version": "1.1.13", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", - "dev": true, "requires": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.4", @@ -18501,6 +18933,20 @@ "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", "dev": true }, + "xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" + }, "xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", @@ -18510,8 +18956,7 @@ "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, "y18n": { "version": "5.0.8", diff --git a/package.json b/package.json index bf9c22d..9ff156c 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ }, "homepage": "https://github.com/Code-Hammers/code-hammers#readme", "dependencies": { + "aws-sdk": "^2.1589.0", "bcryptjs": "^2.4.3", "cookie-parser": "^1.4.6", "dotenv": "^16.3.1", @@ -35,6 +36,7 @@ "html-webpack-plugin": "^5.5.3", "jsonwebtoken": "^9.0.1", "mongoose": "^7.3.4", + "multer": "^1.4.5-lts.1", "react": "^18.2.0", "webpack": "^5.88.1" }, @@ -48,6 +50,7 @@ "@types/jest": "^29.5.3", "@types/jsonwebtoken": "^9.0.2", "@types/mongoose": "^5.11.97", + "@types/multer": "^1.4.11", "@types/node": "^20.5.1", "@types/react": "^18.2.39", "@types/react-test-renderer": "^18.0.7", diff --git a/server/controllers/imageController.ts b/server/controllers/imageController.ts index 57fbe0b..8acbbb0 100644 --- a/server/controllers/imageController.ts +++ b/server/controllers/imageController.ts @@ -1,4 +1,5 @@ -import { Request, Response } from "express"; +import { Request, Response, NextFunction } from "express"; +import Profile from "../models/profileModel"; import AWS from "aws-sdk"; AWS.config.update({ @@ -8,3 +9,68 @@ AWS.config.update({ }); const s3 = new AWS.S3(); + +export const uploadProfilePicture = async ( + req: Request, + res: Response, + next: NextFunction +) => { + if (!req.file) { + return res.status(400).send("No file uploaded."); + } + + const { userID } = req.params; + + const file = req.file as Express.Multer.File; + const s3Key = `profile-pictures/${Date.now()}_${file.originalname}`; + + let params = { + Bucket: process.env.BUCKET_NAME as string, + Key: s3Key, + Body: file.buffer, + ContentType: file.mimetype, + ACL: "private", + }; + + console.log("params.Body", params.Body); + console.log(req.file); + + try { + const uploadResult = await s3.upload(params).promise(); + const updatedProfile = await Profile.findOneAndUpdate( + { user: userID }, + { profilePhoto: s3Key }, + { new: true } + ); + + const presignedUrl = s3.getSignedUrl("getObject", { + Bucket: process.env.BUCKET_NAME, + Key: s3Key, + Expires: 60 * 5, + }); + if (updatedProfile) { + updatedProfile.profilePhoto = presignedUrl; + } + res.status(201).send(updatedProfile); + } catch (err) { + console.error("Error uploading to S3:", err); + } +}; + +//TODO Currently not being used. Built into getProfileByID controller. +export const generatePresignedUrl = (req: Request, res: Response) => { + const key = req.query.key; + const params = { + Bucket: process.env.BUCKET_NAME, + Key: key as string, + Expires: 60, + }; + + s3.getSignedUrl("putObject", params, (err, url) => { + if (err) { + console.error("Error generating URL:", err); + return res.status(500).send("Error generating URL"); + } + res.status(200).send({ message: "URL generated successfully", url }); + }); +}; diff --git a/server/controllers/profileController.ts b/server/controllers/profileController.ts index af31c69..eacc6af 100644 --- a/server/controllers/profileController.ts +++ b/server/controllers/profileController.ts @@ -1,6 +1,15 @@ import Profile from "../models/profileModel"; import { Request, Response, NextFunction } from "express"; import { IProfile } from "../types/profile"; +import AWS from "aws-sdk"; + +AWS.config.update({ + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + region: process.env.AWS_REGION, +}); + +const s3 = new AWS.S3(); // ENDPOINT POST api/profiles/create // PURPOSE Create a new profile @@ -88,6 +97,7 @@ const updateProfile = async ( ) => { const { userID } = req.params; const { fullName, email, personalBio } = req.body; + const newProfile = { fullName, email, @@ -166,9 +176,17 @@ const getProfileById = async ( status: 404, message: { err: "An error occurred during profile retrieval" }, }); - } else { - return res.status(200).json(profile); } + if (profile.profilePhoto) { + const presignedUrl = s3.getSignedUrl("getObject", { + Bucket: process.env.BUCKET_NAME, + Key: profile.profilePhoto, + Expires: 60 * 5, + }); + profile.profilePhoto = presignedUrl; + } + + return res.status(200).json(profile); } catch (error) { return next({ log: "Express error in getProfileById Middleware", diff --git a/server/index.ts b/server/index.ts index a189f85..0250901 100644 --- a/server/index.ts +++ b/server/index.ts @@ -3,6 +3,7 @@ import express, { Request, Response, Application, NextFunction } from "express"; import userRoutes from "./routes/userRoutes"; import profileRoutes from "./routes/profileRoutes"; import authRoutes from "./routes/authRoutes"; +import imageRoutes from "./routes/imageRoutes"; import connectDB from "./config/db"; import dotenv from "dotenv"; import cookieParser from "cookie-parser"; @@ -20,6 +21,7 @@ connectDB(); app.use("/api/users", userRoutes); app.use("/api/profiles", profileRoutes); app.use("/api/auth", authRoutes); +app.use("/api/images", imageRoutes); console.log(`ENV BEFORE CHECK: ${process.env.NODE_ENV}`); diff --git a/server/routes/imageRoutes.ts b/server/routes/imageRoutes.ts new file mode 100644 index 0000000..1eb11a0 --- /dev/null +++ b/server/routes/imageRoutes.ts @@ -0,0 +1,23 @@ +import express from "express"; +import { + uploadProfilePicture, + generatePresignedUrl, +} from "../controllers/imageController"; +import { protect } from "../middleware/authMiddleware"; + +const router = express.Router(); + +import multer from "multer"; +const storage = multer.memoryStorage(); +const upload = multer({ storage: storage }); + +router.post( + "/profile-picture/:userID", + upload.single("profilePicture"), + uploadProfilePicture +); + +//TODO Not currently being used +router.get("/generate-url", generatePresignedUrl); + +export default router; From 693a6a7d8e1564ad5f6494a8cc76f5488a9d8223 Mon Sep 17 00:00:00 2001 From: Brok3Turtl3 Date: Wed, 3 Apr 2024 08:39:51 -0400 Subject: [PATCH 17/29] CHE-54 Adjusted route to look for a user ID param --- client/src/AuthenticatedApp.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/AuthenticatedApp.tsx b/client/src/AuthenticatedApp.tsx index 7bc994c..8806e00 100644 --- a/client/src/AuthenticatedApp.tsx +++ b/client/src/AuthenticatedApp.tsx @@ -44,7 +44,7 @@ const AuthenticatedApp = () => { } /> } /> - } /> + } /> } /> } /> } /> From aece2bc8d2b367a0afe4bfb800e29db83df47dca Mon Sep 17 00:00:00 2001 From: Brok3Turtl3 Date: Wed, 3 Apr 2024 09:44:16 -0400 Subject: [PATCH 18/29] CHE-54 Added Links to individual profile thumbs and adjusted Profile page to display basic information based on userId param passed in URL --- client/src/pages/Profile/Profile.tsx | 10 +++++++--- client/src/pages/Profiles/Profiles.tsx | 8 +++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/client/src/pages/Profile/Profile.tsx b/client/src/pages/Profile/Profile.tsx index 289b7c5..d8780ae 100644 --- a/client/src/pages/Profile/Profile.tsx +++ b/client/src/pages/Profile/Profile.tsx @@ -1,19 +1,23 @@ import React, { useEffect } from "react"; +import { useParams } from "react-router-dom"; import { useAppSelector, useAppDispatch } from "../../app/hooks"; import { fetchUserProfile } from "../../features/userProfile/userProfileSlice"; const Profile = (): JSX.Element => { const dispatch = useAppDispatch(); + const { userId } = useParams(); const userProfile = useAppSelector((state) => state.userProfile.profile); - const user = useAppSelector((state) => state.user.userData); useEffect(() => { - if (user?._id) dispatch(fetchUserProfile(user._id)); + console.log("userId", userId); + if (userId) dispatch(fetchUserProfile(userId)); }, [dispatch]); return (

Profile

-

{user?._id}

+

+ {userProfile?.user.toString()} +

{userProfile?.fullName}

); diff --git a/client/src/pages/Profiles/Profiles.tsx b/client/src/pages/Profiles/Profiles.tsx index 9795e37..1db53c6 100644 --- a/client/src/pages/Profiles/Profiles.tsx +++ b/client/src/pages/Profiles/Profiles.tsx @@ -1,4 +1,5 @@ import React, { useEffect } from "react"; +import { Link } from "react-router-dom"; import { useAppDispatch, useAppSelector } from "../../app/hooks"; import { fetchProfiles } from "../../features/profiles/profilesSlice"; import ProfileThumb from "../../components/ProfileThumb/ProfileThumb"; @@ -17,9 +18,10 @@ const Profiles = (): JSX.Element => {

PROFILES

- {/* TODO Look at better key for this */} - {profiles.map((profile, index) => ( - + {profiles.map((profile) => ( + + + ))}
From db6a15fe91aed9130bfddec9fb7db3084524e577 Mon Sep 17 00:00:00 2001 From: Brok3Turtl3 Date: Wed, 3 Apr 2024 10:49:14 -0400 Subject: [PATCH 19/29] CHE-54 Adjusted tests for new Profiles and Profile structure --- client/src/pages/Profile/Profile.test.tsx | 27 +++++++---- client/src/pages/Profiles/Profiles.test.tsx | 5 +- client/src/pages/Profiles/Profiles.tsx | 4 +- .../__snapshots__/Profiles.test.tsx.snap | 46 +++++++++++-------- 4 files changed, 52 insertions(+), 30 deletions(-) diff --git a/client/src/pages/Profile/Profile.test.tsx b/client/src/pages/Profile/Profile.test.tsx index edd9cd3..e92ff1c 100644 --- a/client/src/pages/Profile/Profile.test.tsx +++ b/client/src/pages/Profile/Profile.test.tsx @@ -4,29 +4,36 @@ import "@testing-library/jest-dom"; import Profile from "./Profile"; import { useAppSelector, useAppDispatch } from "../../app/hooks"; import { fetchUserProfile } from "../../features/userProfile/userProfileSlice"; +import { useParams } from "react-router-dom"; jest.mock("../../app/hooks", () => ({ useAppDispatch: jest.fn(), useAppSelector: jest.fn(), })); +jest.mock("react-router-dom", () => ({ + ...jest.requireActual("react-router-dom"), + useParams: jest.fn(), +})); + jest.mock("../../features/userProfile/userProfileSlice", () => ({ fetchUserProfile: jest.fn(), })); describe("Profile Component", () => { const mockDispatch = jest.fn(); - const mockUser = { - _id: "12345", - name: "John Doe", + const mockUserId = "123456"; + const mockUserProfile = { + user: mockUserId, + fullName: "John Doe", }; - //TODO MOCK BETTER USERPROFILE DATA + //TODO MOCK BETTER USERPROFILE DATA?? beforeEach(() => { (useAppDispatch as jest.Mock).mockReturnValue(mockDispatch); + (useParams as jest.Mock).mockReturnValue({ userId: mockUserId }); (useAppSelector as jest.Mock).mockImplementation((selector) => selector({ - user: { userData: mockUser }, - userProfile: { profile: null }, + userProfile: { profile: mockUserProfile }, }) ); }); @@ -43,12 +50,14 @@ describe("Profile Component", () => { it("dispatches fetchUserProfile on component mount", () => { render(); - expect(mockDispatch).toHaveBeenCalledWith(fetchUserProfile(mockUser._id)); + expect(mockDispatch).toHaveBeenCalledWith(fetchUserProfile(mockUserId)); }); - it("displays the user's ID", () => { + it("displays the user's fullName and userId", () => { render(); - const userIdDisplay = screen.getByText(mockUser._id); + const userIdDisplay = screen.getByText(mockUserProfile.user.toString()); + const userNameDisplay = screen.getByText(mockUserProfile.fullName); expect(userIdDisplay).toBeInTheDocument(); + expect(userNameDisplay).toBeInTheDocument(); }); }); diff --git a/client/src/pages/Profiles/Profiles.test.tsx b/client/src/pages/Profiles/Profiles.test.tsx index b2b46d0..2966c7f 100644 --- a/client/src/pages/Profiles/Profiles.test.tsx +++ b/client/src/pages/Profiles/Profiles.test.tsx @@ -2,6 +2,7 @@ import React from "react"; import { create } from "react-test-renderer"; import { Provider } from "react-redux"; import configureStore from "redux-mock-store"; +import { BrowserRouter } from "react-router-dom"; import Profiles from "./Profiles"; @@ -27,7 +28,9 @@ describe("MainPage Component", () => { const store = mockStore(initialState); const tree = create( - + + + ).toJSON(); expect(tree).toMatchSnapshot(); diff --git a/client/src/pages/Profiles/Profiles.tsx b/client/src/pages/Profiles/Profiles.tsx index 1db53c6..173b3ac 100644 --- a/client/src/pages/Profiles/Profiles.tsx +++ b/client/src/pages/Profiles/Profiles.tsx @@ -18,8 +18,8 @@ const Profiles = (): JSX.Element => {

PROFILES

- {profiles.map((profile) => ( - + {profiles.map((profile, index) => ( + ))} diff --git a/client/src/pages/Profiles/__snapshots__/Profiles.test.tsx.snap b/client/src/pages/Profiles/__snapshots__/Profiles.test.tsx.snap index 41700c4..08d4388 100644 --- a/client/src/pages/Profiles/__snapshots__/Profiles.test.tsx.snap +++ b/client/src/pages/Profiles/__snapshots__/Profiles.test.tsx.snap @@ -12,26 +12,36 @@ exports[`MainPage Component renders correctly 1`] = `
,
+
+

+

+

+
, ] `; From d16374ad0e76d800ad74a759562f229b9c8f2738 Mon Sep 17 00:00:00 2001 From: Brok3Turtl3 Date: Wed, 3 Apr 2024 11:23:13 -0400 Subject: [PATCH 20/29] CHE-53 Adjusted profile thumbs to display images if present and updated getAllProfiles controller to obtain presigned URLS for S3 --- .../src/components/ProfileThumb/ProfileThumb.tsx | 9 ++++++++- server/controllers/profileController.ts | 16 +++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/client/src/components/ProfileThumb/ProfileThumb.tsx b/client/src/components/ProfileThumb/ProfileThumb.tsx index 764514b..6ea3856 100644 --- a/client/src/components/ProfileThumb/ProfileThumb.tsx +++ b/client/src/components/ProfileThumb/ProfileThumb.tsx @@ -10,7 +10,14 @@ const ProfileThumb = ({ profile }: ProfileThumbProps): JSX.Element => { const dispatch = useAppDispatch(); return ( -
+
+ {profile.profilePhoto && ( + Profile + )}

{profile.fullName}

{profile.personalBio}

diff --git a/server/controllers/profileController.ts b/server/controllers/profileController.ts index eacc6af..1d284d0 100644 --- a/server/controllers/profileController.ts +++ b/server/controllers/profileController.ts @@ -147,7 +147,21 @@ const getAllProfiles = async ( message: { err: "There were no profiles to retrieve" }, }); } else { - return res.status(201).json(profiles); + const processedProfiles = await Promise.all( + profiles.map(async (profile) => { + if (profile.profilePhoto) { + const presignedUrl = s3.getSignedUrl("getObject", { + Bucket: process.env.BUCKET_NAME, + Key: profile.profilePhoto, + Expires: 60 * 5, + }); + profile.profilePhoto = presignedUrl; + } + return profile.toObject(); + }) + ); + + return res.status(201).json(processedProfiles); } } catch (error) { return next({ From ca1e375c269b6f827923a6820c34e3122ec00070 Mon Sep 17 00:00:00 2001 From: Brok3Turtl3 Date: Wed, 3 Apr 2024 11:33:15 -0400 Subject: [PATCH 21/29] CHE-53 Updated snapshot for tests --- .../src/pages/Profiles/__snapshots__/Profiles.test.tsx.snap | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/pages/Profiles/__snapshots__/Profiles.test.tsx.snap b/client/src/pages/Profiles/__snapshots__/Profiles.test.tsx.snap index 08d4388..5be788e 100644 --- a/client/src/pages/Profiles/__snapshots__/Profiles.test.tsx.snap +++ b/client/src/pages/Profiles/__snapshots__/Profiles.test.tsx.snap @@ -17,7 +17,7 @@ exports[`MainPage Component renders correctly 1`] = ` onClick={[Function]} >

Date: Tue, 16 Apr 2024 16:41:02 -0400 Subject: [PATCH 22/29] CHE-59 Added basic styling to Profiles and ProfileThumb --- .../components/ProfileThumb/ProfileThumb.tsx | 22 +++++++++---------- client/src/pages/Profiles/Profiles.tsx | 21 +++++++++++------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/client/src/components/ProfileThumb/ProfileThumb.tsx b/client/src/components/ProfileThumb/ProfileThumb.tsx index 6ea3856..892b525 100644 --- a/client/src/components/ProfileThumb/ProfileThumb.tsx +++ b/client/src/components/ProfileThumb/ProfileThumb.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { useAppDispatch } from "../../app/hooks"; import { IProfile } from "../../../types/profile"; interface ProfileThumbProps { @@ -7,19 +6,18 @@ interface ProfileThumbProps { } const ProfileThumb = ({ profile }: ProfileThumbProps): JSX.Element => { - const dispatch = useAppDispatch(); + const defaultImage = "https://picsum.photos/200"; return ( -
- {profile.profilePhoto && ( - Profile - )} -

{profile.fullName}

-

{profile.personalBio}

+
+ {profile.fullName + +

{profile.fullName}

+

{profile.personalBio}

); }; diff --git a/client/src/pages/Profiles/Profiles.tsx b/client/src/pages/Profiles/Profiles.tsx index 173b3ac..6d3ba6d 100644 --- a/client/src/pages/Profiles/Profiles.tsx +++ b/client/src/pages/Profiles/Profiles.tsx @@ -14,15 +14,20 @@ const Profiles = (): JSX.Element => { return ( <> -
+

PROFILES

-
-
- {profiles.map((profile, index) => ( - - - - ))} + +
+ {profiles.map((profile, index) => ( + + + + ))} +
); From f97e83c054959d6ad0a17ef888b107382c653a7d Mon Sep 17 00:00:00 2001 From: Brok3Turtl3 Date: Tue, 16 Apr 2024 17:21:20 -0400 Subject: [PATCH 23/29] CHE-59 snapshot replaced --- .../__snapshots__/Profiles.test.tsx.snap | 47 ------------------- 1 file changed, 47 deletions(-) delete mode 100644 client/src/pages/Profiles/__snapshots__/Profiles.test.tsx.snap diff --git a/client/src/pages/Profiles/__snapshots__/Profiles.test.tsx.snap b/client/src/pages/Profiles/__snapshots__/Profiles.test.tsx.snap deleted file mode 100644 index 5be788e..0000000 --- a/client/src/pages/Profiles/__snapshots__/Profiles.test.tsx.snap +++ /dev/null @@ -1,47 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`MainPage Component renders correctly 1`] = ` -[ -
-

- PROFILES -

-
, - , -] -`; From a2f267ebb8a3177ede818f174cd8b021ed93a091 Mon Sep 17 00:00:00 2001 From: Brok3Turtl3 Date: Tue, 16 Apr 2024 18:20:31 -0400 Subject: [PATCH 24/29] CHE-60 Added basic styling to the Profile page --- client/src/pages/Profile/Profile.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/client/src/pages/Profile/Profile.tsx b/client/src/pages/Profile/Profile.tsx index d8780ae..37f66d6 100644 --- a/client/src/pages/Profile/Profile.tsx +++ b/client/src/pages/Profile/Profile.tsx @@ -13,12 +13,17 @@ const Profile = (): JSX.Element => { if (userId) dispatch(fetchUserProfile(userId)); }, [dispatch]); return ( -
+

Profile

-

- {userProfile?.user.toString()} -

-

{userProfile?.fullName}

+
+ Profile +

{userProfile?.fullName}

+

{userProfile?.personalBio}

+
); }; From 9bd37375ed4a44d8d1dffc9a5966a934ab8422c4 Mon Sep 17 00:00:00 2001 From: Brok3Turtl3 Date: Tue, 16 Apr 2024 18:36:28 -0400 Subject: [PATCH 25/29] CHE-60 Updated testing to reflect new component logic and structure --- client/src/pages/Profile/Profile.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/pages/Profile/Profile.test.tsx b/client/src/pages/Profile/Profile.test.tsx index e92ff1c..2ae255d 100644 --- a/client/src/pages/Profile/Profile.test.tsx +++ b/client/src/pages/Profile/Profile.test.tsx @@ -53,11 +53,11 @@ describe("Profile Component", () => { expect(mockDispatch).toHaveBeenCalledWith(fetchUserProfile(mockUserId)); }); - it("displays the user's fullName and userId", () => { + it("displays the user's fullName", () => { render(); - const userIdDisplay = screen.getByText(mockUserProfile.user.toString()); + const userNameDisplay = screen.getByText(mockUserProfile.fullName); - expect(userIdDisplay).toBeInTheDocument(); + expect(userNameDisplay).toBeInTheDocument(); }); }); From 1f7bf6430d569bdd8712aae2a17bce95984ee9cc Mon Sep 17 00:00:00 2001 From: Brok3Turtl3 Date: Wed, 17 Apr 2024 10:00:51 -0400 Subject: [PATCH 26/29] CHE-61 Hooked up EditProfilePage and added basic styling --- client/src/components/Header/Header.tsx | 4 +- .../pages/EditProfilePage/EditProfilePage.tsx | 89 +++++++------------ 2 files changed, 35 insertions(+), 58 deletions(-) diff --git a/client/src/components/Header/Header.tsx b/client/src/components/Header/Header.tsx index 3e9b71c..b7e21cf 100644 --- a/client/src/components/Header/Header.tsx +++ b/client/src/components/Header/Header.tsx @@ -90,11 +90,11 @@ const Header = (): JSX.Element => { href="#!" className="block px-4 py-2 text-sm text-white hover:bg-gray-800" onClick={() => { - navigate("/app/profile"); + navigate("/app/editProfile"); setShowDropdown(false); }} > - Go to Profile + Edit Profile { } return ( -
-
- {profile?.profilePhoto && ( -
- Profile -
- )} -

- Edit Profile -

-
-
- +
+

Edit Profile

+
+ + {profile?.profilePhoto && ( +
+ Profile +
+ )} +
-
- + +
-
- + +
- -
- -
+ + -
+

Upload Profile Picture