Skip to content

Commit 5085cc8

Browse files
committed
dynamic user routing works now I think
1 parent 24044bd commit 5085cc8

File tree

8 files changed

+193
-51
lines changed

8 files changed

+193
-51
lines changed

csm_web/frontend/src/components/App.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { emptyRoles, Roles } from "../utils/user";
88
import CourseMenu from "./CourseMenu";
99
import Home from "./Home";
1010
import Policies from "./Policies";
11+
import UserProfile from "./UserProfile";
1112
import { DataExport } from "./data_export/DataExport";
1213
import { EnrollmentMatcher } from "./enrollment_automation/EnrollmentMatcher";
1314
import { Resources } from "./resource_aggregation/Resources";
@@ -42,8 +43,12 @@ const App = () => {
4243
<Route path="policies/*" element={<Policies />} />
4344
<Route path="export/*" element={<DataExport />} />
4445
{
45-
// TODO: add route for profiles (/user/:id/* element = {UserProfile})
46+
// TODO: add route for profiles (/profile/:id/* element = {UserProfile})
47+
// TODO: add route for your own profile /profile/*
48+
// reference Section
4649
}
50+
<Route path="profile/*" element={<UserProfile />} />
51+
<Route path="profile/:id/*" element={<UserProfile />} />
4752
<Route path="*" element={<NotFound />} />
4853
</Route>
4954
</Routes>
@@ -82,7 +87,7 @@ function Header(): React.ReactElement {
8287
};
8388

8489
/**
85-
* Helper function to determine class name for the home NavLInk component;
90+
* Helper function to determine class name for the home NavLnk component;
8691
* is always active unless we're in another tab.
8792
*/
8893
const homeNavlinkClass = () => {
@@ -143,6 +148,9 @@ function Header(): React.ReactElement {
143148
<NavLink to="/policies" className={navlinkClassSubtitle}>
144149
<h3 className="site-subtitle">Policies</h3>
145150
</NavLink>
151+
<NavLink to="/profile" className={navlinkClassSubtitle}>
152+
<h3 className="site-subtitle">Profile</h3>
153+
</NavLink>
146154
<a id="logout-btn" href="/logout" title="Log out">
147155
<LogOutIcon className="icon" />
148156
</a>

csm_web/frontend/src/components/CourseMenu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import React, { useEffect, useState } from "react";
33
import { Link, Route, Routes } from "react-router-dom";
44

55
import { DEFAULT_LONG_LOCALE_OPTIONS, DEFAULT_TIMEZONE } from "../utils/datetime";
6-
import { useUserInfo } from "../utils/queries/base";
76
import { useCourses } from "../utils/queries/courses";
7+
import { useUserInfo } from "../utils/queries/user";
88
import { Course as CourseType, UserInfo } from "../utils/types";
99
import LoadingSpinner from "./LoadingSpinner";
1010
import Course from "./course/Course";
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import React from "react";
2+
import { useParams } from "react-router-dom";
3+
import { useUser } from "../utils/queries/user";
4+
import LoadingSpinner from "./LoadingSpinner";
5+
import "../css/base/form.scss"; // Import the base.scss file for styling
6+
import "../css/base/table.scss";
7+
8+
const UserProfile: React.FC = () => {
9+
const { id } = useParams(); // Type the id parameter
10+
const { data, error, isLoading } = useUser(Number(id));
11+
12+
// Handle loading and error states
13+
if (isLoading) {
14+
return <LoadingSpinner className="spinner-centered" />;
15+
}
16+
if (error) {
17+
return <div>Error: {error.message}</div>;
18+
}
19+
20+
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>
29+
</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>
35+
</div>
36+
<div className="form-label">
37+
<label htmlFor="email">Email:</label>
38+
<p id="email" className="form-input">
39+
{data?.email}
40+
</p>
41+
</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>
47+
</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>
53+
</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>
59+
</div>
60+
<div className="form-actions">
61+
<button type="button" className="form-select" onClick={() => console.log("Edit profile")}>
62+
Edit
63+
</button>
64+
</div>
65+
</form>
66+
</div>
67+
);
68+
};
69+
70+
export default UserProfile;

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import "../../css/section.scss";
1313

1414
export default function Section(): React.ReactElement | null {
1515
const { id } = useParams();
16-
1716
const { data: section, isSuccess: sectionLoaded, isError: sectionLoadError } = useSection(Number(id));
1817

1918
if (!sectionLoaded) {

csm_web/frontend/src/utils/queries/base.tsx

Lines changed: 1 addition & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

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

1616
/**
@@ -34,49 +34,3 @@ export const useProfiles = (): UseQueryResult<Profile[], Error> => {
3434
handleError(queryResult);
3535
return queryResult;
3636
};
37-
38-
// TODO: move to new queries/users.tsx
39-
/**
40-
* Hook to get the user's info.
41-
*/
42-
export const useUserInfo = (): UseQueryResult<RawUserInfo, Error> => {
43-
const queryResult = useQuery<RawUserInfo, Error>(
44-
["user"],
45-
async () => {
46-
const response = await fetchNormalized("/user");
47-
if (response.ok) {
48-
return await response.json();
49-
} else {
50-
handlePermissionsError(response.status);
51-
throw new ServerError("Failed to fetch user info");
52-
}
53-
},
54-
{ retry: handleRetry }
55-
);
56-
57-
handleError(queryResult);
58-
return queryResult;
59-
};
60-
61-
// TODO: move to new queries/users.tsx
62-
/**
63-
* Hook to get a list of all user emails.
64-
*/
65-
export const useUserEmails = (): UseQueryResult<string[], Error> => {
66-
const queryResult = useQuery<string[], Error>(
67-
["users"],
68-
async () => {
69-
const response = await fetchNormalized("/users");
70-
if (response.ok) {
71-
return await response.json();
72-
} else {
73-
handlePermissionsError(response.status);
74-
throw new ServerError("Failed to fetch user info");
75-
}
76-
},
77-
{ retry: handleRetry }
78-
);
79-
80-
handleError(queryResult);
81-
return queryResult;
82-
};
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { useQuery, UseQueryResult } from "@tanstack/react-query";
2+
import { fetchNormalized } from "../api";
3+
import { RawUserInfo } from "../types";
4+
import { handleError, handlePermissionsError, handleRetry, ServerError } from "./helpers";
5+
6+
/**
7+
* Hook to get a list of all user emails.
8+
*/
9+
export const useUserEmails = (): UseQueryResult<string[], Error> => {
10+
const queryResult = useQuery<string[], Error>(
11+
["users"],
12+
async () => {
13+
const response = await fetchNormalized("/users");
14+
if (response.ok) {
15+
return await response.json();
16+
} else {
17+
handlePermissionsError(response.status);
18+
throw new ServerError("Failed to fetch user info");
19+
}
20+
},
21+
{ retry: handleRetry }
22+
);
23+
24+
handleError(queryResult);
25+
return queryResult;
26+
};
27+
28+
/**
29+
* Hook to get the current user's info.
30+
*/
31+
// TODO: merge with useUserDetails
32+
export const useUserInfo = (): UseQueryResult<RawUserInfo, Error> => {
33+
const queryResult = useQuery<RawUserInfo, Error>(
34+
["user"],
35+
async () => {
36+
const response = await fetchNormalized("/user");
37+
if (response.ok) {
38+
return await response.json();
39+
} else {
40+
handlePermissionsError(response.status);
41+
throw new ServerError("Failed to fetch user info");
42+
}
43+
},
44+
{ retry: handleRetry }
45+
);
46+
47+
handleError(queryResult);
48+
return queryResult;
49+
};
50+
51+
/**
52+
* Hook to get the requested user's info
53+
*/
54+
// TODO: handle if there is no userId
55+
export const useUserDetails = (userId?: number): UseQueryResult<RawUserInfo, Error> => {
56+
const queryResult = useQuery<RawUserInfo, Error>(
57+
["userDetails", userId],
58+
async () => {
59+
const response = await fetchNormalized(`/user/${userId}`);
60+
if (response.ok) {
61+
return await response.json();
62+
} else {
63+
handlePermissionsError(response.status);
64+
throw new ServerError("Failed to fetch user details");
65+
}
66+
},
67+
{
68+
retry: handleRetry,
69+
enabled: !!userId // only run query if userId is available
70+
}
71+
);
72+
73+
handleError(queryResult);
74+
return queryResult;
75+
};
76+
77+
/**
78+
* Hook to get user info. If userId is provided, fetches details for that user; otherwise, fetches current user's info.
79+
*/
80+
export const useUser = (userId?: number): UseQueryResult<RawUserInfo, Error> => {
81+
const queryKey = userId ? ["userDetails", userId] : ["user"];
82+
83+
const queryResult = useQuery<RawUserInfo, Error>(
84+
queryKey,
85+
async () => {
86+
const endpoint = userId ? `/user/${userId}` : "/user";
87+
const response = await fetchNormalized(endpoint);
88+
if (response.ok) {
89+
return await response.json();
90+
} else {
91+
handlePermissionsError(response.status);
92+
throw new ServerError(userId ? "Failed to fetch user details" : "Failed to fetch user info");
93+
}
94+
},
95+
{
96+
retry: handleRetry,
97+
enabled: userId !== undefined // Only run the query if userId is provided or if fetching current user's info
98+
}
99+
);
100+
101+
handleError(queryResult);
102+
return queryResult;
103+
};

csm_web/frontend/src/utils/types.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ export interface UserInfo {
3636
lastName: string;
3737
email: string;
3838
priorityEnrollment?: DateTime;
39+
bio: string;
40+
pronouns: string;
41+
pronunciation: string;
3942
}
4043

4144
/**

csm_web/scheduler/tests/models/test_user.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pytest
44
from django.urls import reverse
55
from scheduler.factories import (
6+
CoordinatorFactory,
67
CourseFactory,
78
MentorFactory,
89
SectionFactory,
@@ -46,6 +47,7 @@ def fixture_setup_permissions():
4647
# Assign mentors to courses
4748
mentor_a = MentorFactory(user=mentor_user, course=course_a)
4849
mentor_b = MentorFactory(user=other_mentor_user, course=course_b)
50+
coordinator_a = CoordinatorFactory(user=coordinator_user, course=course_a)
4951

5052
# Create sections associated with the correct course via the mentor
5153
section_a1 = SectionFactory(mentor=mentor_a)
@@ -63,6 +65,7 @@ def fixture_setup_permissions():
6365
"mentor_user": mentor_user,
6466
"other_mentor_user": other_mentor_user,
6567
"coordinator_user": coordinator_user,
68+
"coordinator_a": coordinator_a,
6669
"course_a": course_a,
6770
"course_b": course_b,
6871
"section_a1": section_a1,
@@ -137,6 +140,8 @@ def test_student_edit_own_profile(client, setup_permissions):
137140
##############
138141
# Mentor tests
139142
##############
143+
144+
140145
@pytest.mark.django_db
141146
def test_mentor_view_own_profile(client, setup_permissions):
142147
"""

0 commit comments

Comments
 (0)