diff --git a/csm_web/frontend/src/components/App.tsx b/csm_web/frontend/src/components/App.tsx index 841364f28..66ca218cf 100644 --- a/csm_web/frontend/src/components/App.tsx +++ b/csm_web/frontend/src/components/App.tsx @@ -8,6 +8,7 @@ import { emptyRoles, Roles } from "../utils/user"; import CourseMenu from "./CourseMenu"; import Home from "./Home"; import Policies from "./Policies"; +import UserProfile from "./UserProfile"; import { DataExport } from "./data_export/DataExport"; import { EnrollmentMatcher } from "./enrollment_automation/EnrollmentMatcher"; import { Resources } from "./resource_aggregation/Resources"; @@ -41,6 +42,13 @@ const App = () => { } /> } /> } /> + { + // TODO: add route for profiles (/profile/:id/* element = {UserProfile}) + // TODO: add route for your own profile /profile/* + // reference Section + } + } /> + } /> } /> @@ -79,7 +87,7 @@ function Header(): React.ReactElement { }; /** - * Helper function to determine class name for the home NavLInk component; + * Helper function to determine class name for the home NavLnk component; * is always active unless we're in another tab. */ const homeNavlinkClass = () => { @@ -140,6 +148,9 @@ function Header(): React.ReactElement {

Policies

+ +

Profile

+
diff --git a/csm_web/frontend/src/components/CourseMenu.tsx b/csm_web/frontend/src/components/CourseMenu.tsx index a4d67e90e..3796a4692 100644 --- a/csm_web/frontend/src/components/CourseMenu.tsx +++ b/csm_web/frontend/src/components/CourseMenu.tsx @@ -3,8 +3,8 @@ import React, { useEffect, useState } from "react"; import { Link, Route, Routes } from "react-router-dom"; import { DEFAULT_LONG_LOCALE_OPTIONS, DEFAULT_TIMEZONE } from "../utils/datetime"; -import { useUserInfo } from "../utils/queries/base"; import { useCourses } from "../utils/queries/courses"; +import { useUserInfo } from "../utils/queries/profiles"; import { Course as CourseType, UserInfo } from "../utils/types"; import LoadingSpinner from "./LoadingSpinner"; import Course from "./course/Course"; diff --git a/csm_web/frontend/src/components/UserProfile.tsx b/csm_web/frontend/src/components/UserProfile.tsx new file mode 100644 index 000000000..933abd6e0 --- /dev/null +++ b/csm_web/frontend/src/components/UserProfile.tsx @@ -0,0 +1,228 @@ +import React, { useState, useEffect } from "react"; +import { useParams } from "react-router-dom"; +import { PermissionError } from "../utils/queries/helpers"; +import { useUserInfo, useUserInfoUpdateMutation } from "../utils/queries/profiles"; +import LoadingSpinner from "./LoadingSpinner"; + +import "../css/base/form.scss"; +import "../css/base/table.scss"; + +const UserProfile: React.FC = () => { + const { id } = useParams(); + let userId = Number(id); + const { data: currUserData, isError: isCurrUserError, isLoading: currUserIsLoading } = useUserInfo(); + const { data: requestedData, error: requestedError, isLoading: requestedIsLoading } = useUserInfo(userId); + const updateMutation = useUserInfoUpdateMutation(userId); + const [isEditing, setIsEditing] = useState(false); + + const [formData, setFormData] = useState({ + firstName: "", + lastName: "", + bio: "", + pronouns: "", + pronunciation: "" + }); + + const [showSaveSpinner, setShowSaveSpinner] = useState(false); + const [validationText, setValidationText] = useState(""); + + // Populate form data with fetched user data + useEffect(() => { + if (requestedData) { + setFormData({ + firstName: requestedData.firstName || "", + lastName: requestedData.lastName || "", + bio: requestedData.bio || "", + pronouns: requestedData.pronouns || "", + pronunciation: requestedData.pronunciation || "" + }); + } + }, [requestedData]); + + if (requestedIsLoading || currUserIsLoading) { + return ; + } + + if (requestedError || isCurrUserError) { + if (requestedError instanceof PermissionError) { + return

Permission Denied

; + } else { + return

Failed to fetch user data

; + } + } + + if (id === undefined && requestedData) { + userId = requestedData.id; + } + + // Handle input changes + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value + })); + }; + + // Validate current form data + const validateFormData = (): boolean => { + if (!formData.firstName || !formData.lastName) { + setValidationText("First and last names must be specified."); + return false; + } + + setValidationText(""); + return true; + }; + + // Handle form submission + const handleFormSubmit = () => { + if (!validateFormData()) { + return; + } + + setShowSaveSpinner(true); + + updateMutation.mutate( + { + id: userId, + firstName: formData.firstName, + lastName: formData.lastName, + bio: formData.bio, + pronouns: formData.pronouns, + pronunciation: formData.pronunciation + }, + { + onSuccess: () => { + setIsEditing(false); // Exit edit mode after successful save + console.log("Profile updated successfully"); + setShowSaveSpinner(false); + }, + onError: () => { + setValidationText("Error occurred on save."); + setShowSaveSpinner(false); + } + } + ); + }; + + const isCurrUser = currUserData?.id === requestedData?.id || requestedData.isEditable; + + // Toggle edit mode + const handleEditToggle = () => { + setIsEditing(true); + }; + + return ( +
+

User Profile

+
+
+ + {isEditing ? ( + + ) : ( +

{formData.firstName}

+ )} +
+
+ + {isEditing ? ( + + ) : ( +

{formData.lastName}

+ )} +
+
+ + {isEditing ? ( + + ) : ( +

{formData.pronunciation}

+ )} +
+
+ + {isEditing ? ( + + ) : ( +

{formData.pronouns}

+ )} +
+
+ +

{requestedData?.email}

+
+
+ + {isEditing ? ( +