Skip to content

Commit e8e16ad

Browse files
committed
finished profile form
1 parent 5085cc8 commit e8e16ad

File tree

9 files changed

+314
-187
lines changed

9 files changed

+314
-187
lines changed

csm_web/frontend/src/components/CourseMenu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Link, Route, Routes } from "react-router-dom";
44

55
import { DEFAULT_LONG_LOCALE_OPTIONS, DEFAULT_TIMEZONE } from "../utils/datetime";
66
import { useCourses } from "../utils/queries/courses";
7-
import { useUserInfo } from "../utils/queries/user";
7+
import { useUserInfo } from "../utils/queries/profiles";
88
import { Course as CourseType, UserInfo } from "../utils/types";
99
import LoadingSpinner from "./LoadingSpinner";
1010
import Course from "./course/Course";

csm_web/frontend/src/components/UserProfile.tsx

Lines changed: 204 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,226 @@
1-
import React from "react";
1+
import React, { useState, useEffect } from "react";
22
import { useParams } from "react-router-dom";
3-
import { useUser } from "../utils/queries/user";
3+
import { PermissionError } from "../utils/queries/helpers";
4+
import { useUserInfo, useUserInfoUpdateMutation } from "../utils/queries/profiles";
45
import LoadingSpinner from "./LoadingSpinner";
5-
import "../css/base/form.scss"; // Import the base.scss file for styling
6+
7+
import "../css/base/form.scss";
68
import "../css/base/table.scss";
79

810
const UserProfile: React.FC = () => {
9-
const { id } = useParams(); // Type the id parameter
10-
const { data, error, isLoading } = useUser(Number(id));
11+
const { id } = useParams();
12+
let userId = Number(id);
13+
const { data: currUserData, isError: isCurrUserError, isLoading: currUserIsLoading } = useUserInfo();
14+
const { data: requestedData, error: requestedError, isLoading: requestedIsLoading } = useUserInfo(userId);
15+
const updateMutation = useUserInfoUpdateMutation(userId);
16+
const [isEditing, setIsEditing] = useState(false);
17+
18+
const [formData, setFormData] = useState({
19+
firstName: "",
20+
lastName: "",
21+
bio: "",
22+
pronouns: "",
23+
pronunciation: ""
24+
});
25+
26+
const [showSaveSpinner, setShowSaveSpinner] = useState(false);
27+
const [validationText, setValidationText] = useState("");
28+
29+
// Populate form data with fetched user data
30+
useEffect(() => {
31+
if (requestedData) {
32+
setFormData({
33+
firstName: requestedData.firstName || "",
34+
lastName: requestedData.lastName || "",
35+
bio: requestedData.bio || "",
36+
pronouns: requestedData.pronouns || "",
37+
pronunciation: requestedData.pronunciation || ""
38+
});
39+
}
40+
}, [requestedData]);
1141

12-
// Handle loading and error states
13-
if (isLoading) {
42+
if (requestedIsLoading || currUserIsLoading) {
1443
return <LoadingSpinner className="spinner-centered" />;
1544
}
16-
if (error) {
17-
return <div>Error: {error.message}</div>;
45+
46+
if (requestedError || isCurrUserError) {
47+
if (requestedError instanceof PermissionError) {
48+
return <h3>Permission Denied</h3>;
49+
} else {
50+
return <h3>Failed to fetch user data</h3>;
51+
}
52+
}
53+
54+
if (id === undefined && requestedData) {
55+
userId = requestedData.id;
1856
}
1957

58+
// Handle input changes
59+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
60+
const { name, value } = e.target;
61+
setFormData(prev => ({
62+
...prev,
63+
[name]: value
64+
}));
65+
};
66+
67+
// Validate current form data
68+
const validateFormData = (): boolean => {
69+
if (!formData.firstName || !formData.lastName) {
70+
setValidationText("First and last names must be specified.");
71+
return false;
72+
}
73+
74+
setValidationText("");
75+
return true;
76+
};
77+
78+
// Handle form submission
79+
const handleFormSubmit = () => {
80+
if (!validateFormData()) {
81+
return;
82+
}
83+
84+
setShowSaveSpinner(true);
85+
86+
updateMutation.mutate(
87+
{
88+
id: userId,
89+
firstName: formData.firstName,
90+
lastName: formData.lastName,
91+
bio: formData.bio,
92+
pronouns: formData.pronouns,
93+
pronunciation: formData.pronunciation
94+
},
95+
{
96+
onSuccess: () => {
97+
setIsEditing(false); // Exit edit mode after successful save
98+
console.log("Profile updated successfully");
99+
setShowSaveSpinner(false);
100+
},
101+
onError: () => {
102+
setValidationText("Error occurred on save.");
103+
setShowSaveSpinner(false);
104+
}
105+
}
106+
);
107+
};
108+
109+
const isCurrUser = currUserData?.id === requestedData?.id || requestedData.isEditable;
110+
111+
// Toggle edit mode
112+
const handleEditToggle = () => {
113+
setIsEditing(true);
114+
};
115+
20116
return (
21-
<div className="csm-form">
22-
<h2>User Profile</h2>
23-
<form>
24-
<div className="form-label">
25-
<label htmlFor="firstName">First Name:</label>
26-
<p id="firstName" className="form-input">
27-
{data?.firstName}
28-
</p>
117+
<div id="user-profile-form">
118+
<h2 className="form-title">User Profile</h2>
119+
<div className="csm-form">
120+
<div className="form-item">
121+
<label htmlFor="firstName" className="form-label">
122+
First Name:
123+
</label>
124+
{isEditing ? (
125+
<input
126+
type="text"
127+
id="firstName"
128+
name="firstName"
129+
className="form-input"
130+
value={formData.firstName}
131+
onChange={handleInputChange}
132+
required
133+
/>
134+
) : (
135+
<p className="form-static">{formData.firstName}</p>
136+
)}
29137
</div>
30-
<div className="form-label">
31-
<label htmlFor="lastName">Last Name:</label>
32-
<p id="lastName" className="form-input">
33-
{data?.lastName}
34-
</p>
138+
<div className="form-item">
139+
<label htmlFor="lastName" className="form-label">
140+
Last Name:
141+
</label>
142+
{isEditing ? (
143+
<input
144+
type="text"
145+
id="lastName"
146+
name="lastName"
147+
className="form-input"
148+
value={formData.lastName}
149+
onChange={handleInputChange}
150+
required
151+
/>
152+
) : (
153+
<p className="form-static">{formData.lastName}</p>
154+
)}
35155
</div>
36-
<div className="form-label">
37-
<label htmlFor="email">Email:</label>
38-
<p id="email" className="form-input">
39-
{data?.email}
40-
</p>
156+
<div className="form-item">
157+
<label htmlFor="pronunciation" className="form-label">
158+
Pronunciation:
159+
</label>
160+
{isEditing ? (
161+
<input
162+
type="text"
163+
id="pronunciation"
164+
name="pronunciation"
165+
className="form-input"
166+
value={formData.pronunciation}
167+
onChange={handleInputChange}
168+
/>
169+
) : (
170+
<p className="form-static">{formData.pronunciation}</p>
171+
)}
41172
</div>
42-
<div className="form-label">
43-
<label htmlFor="bio">Bio:</label>
44-
<p id="bio" className="form-input">
45-
{data?.bio || "N/A"}
46-
</p>
173+
<div className="form-item">
174+
<label htmlFor="pronouns" className="form-label">
175+
Pronouns:
176+
</label>
177+
{isEditing ? (
178+
<input
179+
type="text"
180+
id="pronouns"
181+
name="pronouns"
182+
className="form-input"
183+
value={formData.pronouns}
184+
onChange={handleInputChange}
185+
/>
186+
) : (
187+
<p className="form-static">{formData.pronouns}</p>
188+
)}
47189
</div>
48-
<div className="form-label">
49-
<label htmlFor="pronouns">Pronouns:</label>
50-
<p id="pronouns" className="form-input">
51-
{data?.pronouns || "N/A"}
52-
</p>
190+
<div className="form-item">
191+
<label htmlFor="email" className="form-label">
192+
Email:
193+
</label>
194+
<p className="form-static">{requestedData?.email}</p>
53195
</div>
54-
<div className="form-label">
55-
<label htmlFor="pronunciation">Pronunciation:</label>
56-
<p id="pronunciation" className="form-input">
57-
{data?.pronunciation || "N/A"}
58-
</p>
196+
<div className="form-item">
197+
<label htmlFor="bio" className="form-label">
198+
Bio:
199+
</label>
200+
{isEditing ? (
201+
<textarea id="bio" name="bio" className="form-input" value={formData.bio} onChange={handleInputChange} />
202+
) : (
203+
<p className="form-static">{formData.bio}</p>
204+
)}
59205
</div>
60206
<div className="form-actions">
61-
<button type="button" className="form-select" onClick={() => console.log("Edit profile")}>
62-
Edit
63-
</button>
207+
{validationText && (
208+
<div className="form-validation-container">
209+
<span className="form-validation-text">{validationText}</span>
210+
</div>
211+
)}
212+
{isCurrUser &&
213+
(isEditing ? (
214+
<button className="primary-btn" onClick={handleFormSubmit} disabled={showSaveSpinner}>
215+
{showSaveSpinner ? <LoadingSpinner /> : "Save"}
216+
</button>
217+
) : (
218+
<button className="primary-btn" onClick={handleEditToggle}>
219+
Edit
220+
</button>
221+
))}
64222
</div>
65-
</form>
223+
</div>
66224
</div>
67225
);
68226
};

csm_web/frontend/src/components/course/CreateSectionModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useEffect, useState } from "react";
22

33
import { DAYS_OF_WEEK } from "../../utils/datetime";
4-
import { useUserEmails } from "../../utils/queries/base";
4+
import { useUserEmails } from "../../utils/queries/profiles";
55
import { useSectionCreateMutation } from "../../utils/queries/sections";
66
import { Spacetime } from "../../utils/types";
77
import Modal from "../Modal";

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { DateTime } from "luxon";
22
import React, { useState } from "react";
33
import { Link } from "react-router-dom";
4-
5-
import { useUserEmails } from "../../utils/queries/base";
4+
import { useUserEmails } from "../../utils/queries/profiles";
65
import { useEnrollStudentMutation } from "../../utils/queries/sections";
76
import LoadingSpinner from "../LoadingSpinner";
87
import Modal from "../Modal";

csm_web/frontend/src/css/base/form.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
border: none;
7272
}
7373

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

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { useMutation, UseMutationResult, useQuery, useQueryClient, UseQueryResult } from "@tanstack/react-query";
2+
3+
import { fetchNormalized, fetchWithMethod, HTTP_METHODS } from "../api";
4+
import { RawUserInfo } from "../types";
5+
import { handleError, handlePermissionsError, handleRetry, ServerError } from "./helpers";
6+
7+
/**
8+
* Hook to get a list of all user emails.
9+
*/
10+
export const useUserEmails = (): UseQueryResult<string[], Error> => {
11+
const queryResult = useQuery<string[], Error>(
12+
["users"],
13+
async () => {
14+
const response = await fetchNormalized("/users");
15+
if (response.ok) {
16+
return await response.json();
17+
} else {
18+
handlePermissionsError(response.status);
19+
throw new ServerError("Failed to fetch user info");
20+
}
21+
},
22+
{ retry: handleRetry }
23+
);
24+
25+
handleError(queryResult);
26+
return queryResult;
27+
};
28+
29+
/**
30+
* Hook to get user info. If userId is provided, fetches details for that user;
31+
* otherwise, fetches current user's info.
32+
*/
33+
export const useUserInfo = (userId?: number): UseQueryResult<RawUserInfo, Error> => {
34+
const queryKey = userId ? ["userDetails", userId] : ["user"];
35+
36+
const queryResult = useQuery<RawUserInfo, Error>(
37+
queryKey,
38+
async () => {
39+
const endpoint = userId ? `/user/${userId}` : "/user";
40+
const response = await fetchNormalized(endpoint);
41+
if (response.ok) {
42+
return await response.json();
43+
} else {
44+
handlePermissionsError(response.status);
45+
throw new ServerError(userId ? "Failed to fetch user details" : "Failed to fetch user info");
46+
}
47+
},
48+
{
49+
retry: handleRetry
50+
}
51+
);
52+
53+
handleError(queryResult);
54+
return queryResult;
55+
};
56+
57+
/**
58+
* Hook to update a user's profile information.
59+
*/
60+
export const useUserInfoUpdateMutation = (
61+
userId: number
62+
): UseMutationResult<void, ServerError, Partial<RawUserInfo>> => {
63+
const queryClient = useQueryClient();
64+
const mutationResult = useMutation<void, Error, Partial<RawUserInfo>>(
65+
async (body: Partial<RawUserInfo>) => {
66+
const response = await fetchWithMethod(`/user/${userId}/update`, HTTP_METHODS.PUT, body);
67+
if (response.ok) {
68+
return;
69+
} else {
70+
handlePermissionsError(response.status);
71+
throw new ServerError(`Failed to update user profile with ID ${userId}`);
72+
}
73+
},
74+
{
75+
onSuccess: () => {
76+
// Invalidate queries related to the user's profile to ensure fresh data
77+
queryClient.invalidateQueries(["userProfile", userId]);
78+
},
79+
retry: handleRetry
80+
}
81+
);
82+
83+
handleError(mutationResult);
84+
return mutationResult;
85+
};

0 commit comments

Comments
 (0)