Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implemented backend logic for retrieving and updating a user profile + basic frontend #492

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
13 changes: 12 additions & 1 deletion csm_web/frontend/src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -41,6 +42,13 @@ const App = () => {
<Route path="matcher/*" element={<EnrollmentMatcher />} />
<Route path="policies/*" element={<Policies />} />
<Route path="export/*" element={<DataExport />} />
{
// TODO: add route for profiles (/profile/:id/* element = {UserProfile})
// TODO: add route for your own profile /profile/*
// reference Section
}
<Route path="profile/*" element={<UserProfile />} />
<Route path="profile/:id/*" element={<UserProfile />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
Expand Down Expand Up @@ -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 = () => {
Expand Down Expand Up @@ -140,6 +148,9 @@ function Header(): React.ReactElement {
<NavLink to="/policies" className={navlinkClassSubtitle}>
<h3 className="site-subtitle">Policies</h3>
</NavLink>
<NavLink to="/profile" className={navlinkClassSubtitle}>
<h3 className="site-subtitle">Profile</h3>
</NavLink>
<a id="logout-btn" href="/logout" title="Log out">
<LogOutIcon className="icon" />
</a>
Expand Down
2 changes: 1 addition & 1 deletion csm_web/frontend/src/components/CourseMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
228 changes: 228 additions & 0 deletions csm_web/frontend/src/components/UserProfile.tsx
Original file line number Diff line number Diff line change
@@ -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 <LoadingSpinner className="spinner-centered" />;
}

if (requestedError || isCurrUserError) {
if (requestedError instanceof PermissionError) {
return <h3>Permission Denied</h3>;
} else {
return <h3>Failed to fetch user data</h3>;
}
}

if (id === undefined && requestedData) {
userId = requestedData.id;
}

// Handle input changes
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
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 (
<div id="user-profile-form">
<h2 className="form-title">User Profile</h2>
<div className="csm-form">
<div className="form-item">
<label htmlFor="firstName" className="form-label">
First Name:
</label>
{isEditing ? (
<input
type="text"
id="firstName"
name="firstName"
className="form-input"
value={formData.firstName}
onChange={handleInputChange}
required
/>
) : (
<p className="form-static">{formData.firstName}</p>
)}
</div>
<div className="form-item">
<label htmlFor="lastName" className="form-label">
Last Name:
</label>
{isEditing ? (
<input
type="text"
id="lastName"
name="lastName"
className="form-input"
value={formData.lastName}
onChange={handleInputChange}
required
/>
) : (
<p className="form-static">{formData.lastName}</p>
)}
</div>
<div className="form-item">
<label htmlFor="pronunciation" className="form-label">
Pronunciation:
</label>
{isEditing ? (
<input
type="text"
id="pronunciation"
name="pronunciation"
className="form-input"
value={formData.pronunciation}
onChange={handleInputChange}
/>
) : (
<p className="form-static">{formData.pronunciation}</p>
)}
</div>
<div className="form-item">
<label htmlFor="pronouns" className="form-label">
Pronouns:
</label>
{isEditing ? (
<input
type="text"
id="pronouns"
name="pronouns"
className="form-input"
value={formData.pronouns}
onChange={handleInputChange}
/>
) : (
<p className="form-static">{formData.pronouns}</p>
)}
</div>
<div className="form-item">
<label htmlFor="email" className="form-label">
Email:
</label>
<p className="form-static">{requestedData?.email}</p>
</div>
<div className="form-item">
<label htmlFor="bio" className="form-label">
Bio:
</label>
{isEditing ? (
<textarea id="bio" name="bio" className="form-input" value={formData.bio} onChange={handleInputChange} />
) : (
<p className="form-static">{formData.bio}</p>
)}
</div>
<div className="form-actions">
{validationText && (
<div className="form-validation-container">
<span className="form-validation-text">{validationText}</span>
</div>
)}
{isCurrUser &&
(isEditing ? (
<button className="primary-btn" onClick={handleFormSubmit} disabled={showSaveSpinner}>
{showSaveSpinner ? <LoadingSpinner /> : "Save"}
</button>
) : (
<button className="primary-btn" onClick={handleEditToggle}>
Edit
</button>
))}
</div>
</div>
</div>
);
};

export default UserProfile;
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react";

import { DAYS_OF_WEEK } from "../../utils/datetime";
import { useUserEmails } from "../../utils/queries/base";
import { useUserEmails } from "../../utils/queries/profiles";
import { useSectionCreateMutation } from "../../utils/queries/sections";
import { Spacetime } from "../../utils/types";
import Modal from "../Modal";
Expand Down
12 changes: 8 additions & 4 deletions csm_web/frontend/src/components/course/SectionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,17 @@ export const SectionCard = ({
const iconWidth = "8em";
const iconHeight = "8em";
if (enrollmentSuccessful) {
const inlineIconWidth = "1.3em";
const inlineIconHeight = "1.3em";
return (
<div className="enroll-confirm-modal-contents">
<CheckCircle height={iconHeight} width={iconWidth} />
<h3>Successfully enrolled</h3>
<ModalCloser>
<button className="primary-btn">OK</button>
</ModalCloser>
<h4>To view and update your profile, click the button below</h4>
<Link className="primary-btn" to="/profile">
<UserIcon width={inlineIconWidth} height={inlineIconHeight} />
Profile
</Link>
</div>
);
}
Expand Down Expand Up @@ -168,7 +172,7 @@ export const SectionCard = ({
)}
</p>
<p title="Mentor">
<UserIcon width={iconWidth} height={iconHeight} /> {mentor.name}
<UserIcon width={iconWidth} height={iconHeight} /> <Link to={`/profile/${mentor.id}`}>{mentor.name}</Link>
</p>
<p title="Current enrollment">
<GroupIcon width={iconWidth} height={iconHeight} /> {`${numStudentsEnrolled}/${capacity}`}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { DateTime } from "luxon";
import React, { useState } from "react";
import { Link } from "react-router-dom";

import { useUserEmails } from "../../utils/queries/base";
import { useUserEmails } from "../../utils/queries/profiles";
import { useEnrollStudentMutation } from "../../utils/queries/sections";
import LoadingSpinner from "../LoadingSpinner";
import Modal from "../Modal";
Expand Down Expand Up @@ -39,7 +38,7 @@
progress?: Array<{
email: string;
status: string;
detail?: any;

Check warning on line 41 in csm_web/frontend/src/components/section/CoordinatorAddStudentModal.tsx

View workflow job for this annotation

GitHub Actions / ESLint

csm_web/frontend/src/components/section/CoordinatorAddStudentModal.tsx#L41

Unexpected any. Specify a different type (@typescript-eslint/no-explicit-any)
}>;
}

Expand Down
1 change: 0 additions & 1 deletion csm_web/frontend/src/components/section/Section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import "../../css/section.scss";

export default function Section(): React.ReactElement | null {
const { id } = useParams();

const { data: section, isSuccess: sectionLoaded, isError: sectionLoadError } = useSection(Number(id));

if (!sectionLoaded) {
Expand Down
2 changes: 1 addition & 1 deletion csm_web/frontend/src/css/base/form.scss
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
border: none;
}

/* Neccessary for options to be legible on Windows */
/* Necessary for options to be legible on Windows */
.form-select > option {
color: black;

Expand Down
Loading
Loading