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 #481

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
46 changes: 1 addition & 45 deletions csm_web/frontend/src/utils/queries/base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import { useQuery, UseQueryResult } from "@tanstack/react-query";
import { fetchNormalized } from "../api";
import { Profile, RawUserInfo } from "../types";
import { Profile } from "../types";
import { handleError, handlePermissionsError, handleRetry, ServerError } from "./helpers";

/**
Expand All @@ -34,47 +34,3 @@ export const useProfiles = (): UseQueryResult<Profile[], Error> => {
handleError(queryResult);
return queryResult;
};

/**
* Hook to get the user's info.
*/
export const useUserInfo = (): UseQueryResult<RawUserInfo, Error> => {
const queryResult = useQuery<RawUserInfo, Error>(
["userinfo"],
async () => {
const response = await fetchNormalized("/userinfo");
if (response.ok) {
return await response.json();
} else {
handlePermissionsError(response.status);
throw new ServerError("Failed to fetch user info");
}
},
{ retry: handleRetry }
);

handleError(queryResult);
return queryResult;
};

/**
* Hook to get a list of all user emails.
*/
export const useUserEmails = (): UseQueryResult<string[], Error> => {
const queryResult = useQuery<string[], Error>(
["users"],
async () => {
const response = await fetchNormalized("/users");
if (response.ok) {
return await response.json();
} else {
handlePermissionsError(response.status);
throw new ServerError("Failed to fetch user info");
}
},
{ retry: handleRetry }
);

handleError(queryResult);
return queryResult;
};
Loading
Loading