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

Add major selection to profile #501

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
12 changes: 10 additions & 2 deletions client/src/modules/Course/Components/MultiSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';

// CSS FILES
import styles from '../Styles/Select.module.css';
Expand All @@ -9,11 +9,18 @@ const MultiSelect = ({
value,
options,
placeholder,
onChange
onChange,
preselectedOptions,
}: SelectProps) => {
const [searchTerm, setSearchTerm] = useState<string>('');
const [open, setOpen] = useState<boolean>(false);

useEffect(() => {
if (preselectedOptions) {
onChange(preselectedOptions);
}
}, [preselectedOptions]);

const filteredOptions = searchTerm.length !== 0
? options.filter((option) =>
option.toLowerCase().includes(searchTerm.toLowerCase())
Expand Down Expand Up @@ -110,6 +117,7 @@ type SelectProps = {
placeholder: string;
value: string[];
onChange: (selectedOptions: string[]) => void;
preselectedOptions?: string[];
};

export default MultiSelect;
54 changes: 41 additions & 13 deletions client/src/modules/Course/Components/ReviewModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react';
import axios from 'axios';

import MultiSelect from './MultiSelect';
import SingleSelect from './SingleSelect';
Expand All @@ -14,11 +15,11 @@ import LoginModal from './LoginModal';
import { useAuthOptionalLogin } from '../../../auth/auth_utils';

const ReviewModal = ({
open,
setReviewOpen,
submitReview,
professorOptions
}: Modal) => {
open,
setReviewOpen,
submitReview,
professorOptions
}: Modal) => {
// Modal Logic
function closeModal() {
setReviewOpen(false);
Expand Down Expand Up @@ -59,7 +60,9 @@ const ReviewModal = ({

const [loginModalOpen, setLoginModalOpen] = useState<boolean>(false);

const { isLoggedIn, signIn } = useAuthOptionalLogin();
const [userMajors, setUserMajors] = useState<string[]>([]);
const [loadingMajors, setLoadingMajors] = useState<boolean>(true);
const { isLoggedIn, netId, signIn } = useAuthOptionalLogin();

const [valid, setValid] = useState<Valid>({
professor: false,
Expand All @@ -69,6 +72,18 @@ const ReviewModal = ({
});
const [allowSubmit, setAllowSubmit] = useState<boolean>(false);

useEffect(() => {
setLoadingMajors(true)
if (isLoggedIn) {
getUserMajors();
} else {
setLoadingMajors(false)
}
if (!isLoggedIn || userMajors.length === 0) {
setValid({ ...valid, major: true });
}
}, [isLoggedIn, open]);

useEffect(() => {
if (!professorOptions.includes('Not Listed')) {
professorOptions.push('Not Listed');
Expand All @@ -79,6 +94,16 @@ const ReviewModal = ({
setAllowSubmit(valid.professor && valid.major && valid.grade && valid.text);
}, [valid]);

const getUserMajors = async () => {
const response = await axios.post('/api/profiles/get-majors', {
netId
})
if (response.status === 200) {
setUserMajors(response.data.majors)
setLoadingMajors(false)
}
}

const onProfessorChange = (newSelectedProfessors: string[]) => {
setSelectedProfessors(newSelectedProfessors);
if (newSelectedProfessors.length > 0)
Expand Down Expand Up @@ -123,7 +148,7 @@ const ReviewModal = ({
text: reviewText,
isCovid: false,
grade: selectedGrade,
major: selectedMajors
major: selectedMajors ? selectedMajors : [],
};
submitReview(newReview);
}
Expand Down Expand Up @@ -210,18 +235,21 @@ const ReviewModal = ({
isOverall={false}
/>
</div>
<MultiSelect
options={majorOptions}
value={selectedMajors}
onChange={onMajorChange}
placeholder="Major"
/>
<SingleSelect
options={gradeoptions}
value={selectedGrade}
onChange={onGradeChange}
placeholder="Grade Received"
/>
{!loadingMajors && (!isLoggedIn || userMajors.length === 0) && (
<MultiSelect
options={majorOptions}
value={selectedMajors}
onChange={onMajorChange}
preselectedOptions={userMajors}
placeholder="Major"
/>
)}
</div>
<div className={styles.textcol}>
<textarea
Expand Down
21 changes: 20 additions & 1 deletion client/src/modules/Profile/Component/Profile.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { Redirect } from 'react-router-dom';

import axios from 'axios';
Expand All @@ -19,6 +19,7 @@ import type { NewReview } from '../../../types';

import { useAuthMandatoryLogin } from '../../../auth/auth_utils';
import { randomPicture } from '../../Globals/profile_picture';
import formatList from 'common/formatList'

import styles from '../Styles/Profile.module.css';

Expand All @@ -29,6 +30,7 @@ const Profile = () => {
const [pendingReviews, setPendingReviews] = useState<ReviewType[]>([]);
const [approvedReviews, setApprovedReviews] = useState<ReviewType[]>([]);

const savedSessionReview = useRef<NewReview | null>(null);
const [upvoteCount, setUpvoteCount] = useState(0);

const { isLoggedIn, token, netId, isAuthenticating, signOut } =
Expand Down Expand Up @@ -128,10 +130,26 @@ const Profile = () => {
}
}

const checkForUpdatedMajor = async (review: NewReview) => {
const getMajorsReq = await axios.post('/api/profiles/get-majors', { netId })
if (getMajorsReq.status === 200) {
const oldMajors = getMajorsReq.data.majors
if (oldMajors && review &&
JSON.stringify(oldMajors) !== JSON.stringify(review.major)) {
toast.info('Your major has been changed ' +
(oldMajors.length !== 0 && 'from ')
+ formatList(oldMajors) + ' to '
+ formatList(review.major) + "."
)
}
}
}

// Only update reviews if we have a given user's netId + they are no longer authenticating.
if (netId && !isAuthenticating) {
getReviews();
getReviewsHelpful();
if (savedSessionReview.current) checkForUpdatedMajor(savedSessionReview.current)
}
}, [netId, isAuthenticating]);

Expand Down Expand Up @@ -186,6 +204,7 @@ const Profile = () => {
sessionCourseId !== '' &&
isLoggedIn
) {
savedSessionReview.current = sessionReview;
submitReview(sessionReview, sessionCourseId);
}
}, [isLoggedIn, token]);
Expand Down
89 changes: 86 additions & 3 deletions client/src/modules/Profile/Component/UserInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import styles from '../Styles/UserInfo.module.css';
import ProfileCard from './ProfileCard';
import MultiSelect from '../../Course/Components/MultiSelect';
import allMajors from '../../Globals/majors';
import formatList from 'common/formatList'
import axios from 'axios';
import { toast } from 'react-toastify';

type UserInfoProps = {
profilePicture: string;
Expand All @@ -17,6 +22,40 @@ const UserInfo = ({
netId,
signOut
}: UserInfoProps) => {
const majorOptions: string[] = allMajors;
const [selectedMajors, setSelectedMajors] = useState<string[]>([]);
const [validMajor, setValidMajor] = useState<boolean>(false);
const [userMajors, setUserMajors] = useState<string[]>([]);
const [showMajorUpdate, setShowMajorUpdate] = useState<boolean>(false);

useEffect(() => {
getUserMajors();
}, []);

const onMajorSelectionChange = (newSelectedMajors: string[]) => {
setSelectedMajors(newSelectedMajors);
setValidMajor(JSON.stringify(newSelectedMajors) !== JSON.stringify(userMajors));
}

const getUserMajors = async () => {
const response = await axios.post('/api/profiles/get-majors', { netId })
if (response.status === 200) {
setUserMajors(response.data.majors)
}
}

const updateMajors = async () => {
const response = await axios.post('/api/profiles/set-majors', { netId, majors: selectedMajors })
console.log(response)
if (response.status === 200) {
setUserMajors(selectedMajors)
setShowMajorUpdate(false)
toast.success(
"Majors successfully updated!"
);
}
}

return (
<div className={styles.container}>
<div className={styles.title}>My Dashboard</div>
Expand All @@ -25,7 +64,15 @@ const UserInfo = ({
src={profilePicture}
alt="user profile bear"
/>
<div className={styles.netid}>{netId}</div>
<div>
<div className={styles.netid}>
<span className={styles.bold}>{netId}</span>
{userMajors.length > 0 && " is studying"}
<p className={styles.bold}>
{formatList(userMajors)}
</p>
</div>
</div>
<div className={styles.subtitle}>User Statistics</div>
<div className={styles.statssection}>
<ProfileCard
Expand All @@ -39,7 +86,43 @@ const UserInfo = ({
image="/helpful_review_icon.svg"
></ProfileCard>
</div>
<button className={styles.signoutbutton} onClick={signOut}>
{!showMajorUpdate && (
<button className={styles.btn} onClick={() => setShowMajorUpdate(true)}>
{userMajors.length === 0
? 'Set your major(s)'
: 'Update your major(s)'}
</button>
)}
{showMajorUpdate && (
<div className={styles.majorcard}>
<MultiSelect
options={majorOptions}
value={selectedMajors}
onChange={onMajorSelectionChange}
preselectedOptions={userMajors}
placeholder="Major"
/>
<div className={styles.halfsizebuttons}>
<button
className={styles.btn}
onClick={updateMajors}
disabled={!validMajor}
title={validMajor ? '' : "You've already selected these majors!"}
>
{selectedMajors.length === 0 && validMajor
? 'Submit (clear)'
: 'Submit'}
</button>
<button
className={styles.btn}
onClick={() => setShowMajorUpdate(false)}
>
Cancel
</button>
</div>
</div>
)}
<button className={styles.btn} onClick={signOut}>
Log Out
</button>
</div>
Expand Down
26 changes: 24 additions & 2 deletions client/src/modules/Profile/Styles/UserInfo.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,20 @@
text-align: center;
}

.bold {
font-weight: 700;
}

.netid {
text-align: center;
color: var(--clr-gray-300);
font-size: 0.85em;
font-size: 1em;
}

.majorcard {
background: var(--clr-gray-100);
border-radius: 10px;
padding: 12px;
}

.statssection {
Expand All @@ -42,7 +52,19 @@
width: 100%;
}

.signoutbutton {
.halfsizebuttons {
display: flex;
width: 100vw;
column-gap: 12px;
padding-top: 1em;
}

.btn:disabled {
background: grey;
cursor: not-allowed;
}

.btn {
width: 100%;
border-radius: 10px;
padding: 12px 0;
Expand Down
13 changes: 13 additions & 0 deletions common/formatList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Formats list into human-readable text.
* eg ["a","b","c"] --> "a, b, and c"
* @param list
*/

const formatList = (list: string[]) => {
return list.join(', ').replace(/(, )(?!.*\1)/,
(list.length > 2 ? ', and ' : ' and ')
);
}

export default formatList;
1 change: 1 addition & 0 deletions common/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface Student {
readonly privilege: string; // user privilege level
reviews: string[];
likedReviews: string[];
majors?: string[];
}

export interface Subject {
Expand Down
3 changes: 2 additions & 1 deletion server/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ const StudentSchema = new Schema<StudentDocument>({
token: { type: String }, // random token generated during login process
privilege: { type: String }, // user privilege level. Takes values "regular" | "admin"
reviews: { type: [String] }, // the reviews that this user has posted.
likedReviews: { type: [String] }
likedReviews: { type: [String] },
majors: { type: [String] },
});
export const Students = mongoose.model<StudentDocument>(
'students',
Expand Down
Loading
Loading