diff --git a/client/src/modules/Course/Components/Course.tsx b/client/src/modules/Course/Components/Course.tsx index 2770fe09..e2386aff 100644 --- a/client/src/modules/Course/Components/Course.tsx +++ b/client/src/modules/Course/Components/Course.tsx @@ -27,6 +27,7 @@ import { Session } from '../../../session-store'; import { useAuthOptionalLogin } from '../../../auth/auth_utils'; import ReviewModal from './ReviewModal'; +import { compareSems } from 'common/CourseCard'; enum PageStatus { Loading, @@ -101,6 +102,14 @@ export const Course = () => { return 0; } + /** + * Sorts reviews based on descending semester (or by date if not available). + */ + const sortBySem = (a: Review, b: Review) => + b.semester && a.semester + ? compareSems(a.semester,b.semester) + : sortByDate(a,b) + /** * Update state to conditionally render sticky bottom-right review button */ @@ -176,6 +185,8 @@ export const Course = () => { setCourseReviews([...courseReviews].sort(sortByDate)); } else if (value === 'professor') { setCourseReviews([...courseReviews].sort(sortByProf)); + } else if (value === 'semester') { + setCourseReviews([...courseReviews].sort(sortBySem)); } } @@ -299,6 +310,7 @@ export const Course = () => { + diff --git a/client/src/modules/Course/Components/PreviewReviewCard.tsx b/client/src/modules/Course/Components/PreviewReviewCard.tsx index 20a3054e..6e6ea83f 100644 --- a/client/src/modules/Course/Components/PreviewReviewCard.tsx +++ b/client/src/modules/Course/Components/PreviewReviewCard.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef } from 'react'; import axios from 'axios'; import { Review as ReviewType } from 'common'; +import { convertSemAbbreviation } from 'common/CourseCard' import styles from '../Styles/ReviewCard.module.css'; import previewstyle from '../Styles/PreviewCard.module.css'; @@ -114,7 +115,11 @@ export default function PreviewReviewCard({ ' ' + courseNum?.toUpperCase() + ' | ' + - professornames} + professornames + + (_review.semester + ? ', ' + convertSemAbbreviation(_review.semester) + : '') + } ); @@ -193,14 +198,9 @@ export default function PreviewReviewCard({ {_review.grade && /^([^0-9]*)$/.test(_review.grade) ? _review.grade - : 'N/A'} + : _review.writtenDuringSemester ? 'N/A (Review written during semester)' : 'N/A' + } - {_review.semester ? ( - <> - {' '} | {' '} - {_review.semester} - - ) : ''}
); @@ -205,14 +211,9 @@ export default function ReviewCard({ {_review.grade && /^([^0-9]*)$/.test(_review.grade) ? _review.grade - : 'N/A'} + : _review.writtenDuringSemester ? 'N/A (Review written during semester)' : 'N/A' + } - {_review.semester ? ( - <> - {' '} | {' '} - {_review.semester} - - ) : ''}
Major{' '} diff --git a/client/src/modules/Course/Components/ReviewModal.tsx b/client/src/modules/Course/Components/ReviewModal.tsx index 6d641434..33b3ea2d 100644 --- a/client/src/modules/Course/Components/ReviewModal.tsx +++ b/client/src/modules/Course/Components/ReviewModal.tsx @@ -5,6 +5,8 @@ import MultiSelect from './MultiSelect'; import SingleSelect from './SingleSelect'; import RatingInput from './RatingInput'; +import { SEMESTER_FORMAT } from 'common/CourseCard' + // CSS FILES import styles from '../Styles/ReviewModal.module.css'; import closeIcon from '../../../assets/icons/X.svg'; @@ -13,6 +15,8 @@ import closeIcon from '../../../assets/icons/X.svg'; import majors from '../../Globals/majors'; import AnonymousWarning from './AnonymousWarning'; import { useAuthOptionalLogin } from '../../../auth/auth_utils'; +import { CURRENT_SEMESTER } from 'common/constants'; +import { compareWithCurrentSem } from 'common/CourseCard'; const ReviewModal = ({ open, @@ -56,6 +60,7 @@ const ReviewModal = ({ const [selectedGrade, setSelectedGrade] = useState(''); const [selectedSem, setSelectedSem] = useState(''); const [reviewText, setReviewText] = useState(''); + const [isCurrentSem, setIsCurrentSem] = useState(false); const [overall, setOverall] = useState(3); const [difficulty, setDifficulty] = useState(3); @@ -75,8 +80,6 @@ const ReviewModal = ({ }); const [allowSubmit, setAllowSubmit] = useState(false); - const SEMESTER_FORMAT: RegExp = new RegExp('^(SP|SU|WI|FA)\\d{2}$'); - useEffect(() => { if (!professorOptions.includes('Not Listed')) { professorOptions.push('Not Listed'); @@ -84,12 +87,16 @@ const ReviewModal = ({ }, [professorOptions]); useEffect(() => { - setAllowSubmit(valid.professor && valid.semester && valid.major && valid.grade && valid.text); + setAllowSubmit(valid.professor && valid.semester && valid.major && (valid.grade !== isCurrentSem) && valid.text); if (isLoggedIn) { getNoReviews(); } }, [valid]); + useEffect(() => { + if (isCurrentSem) onGradeChange(''); + }, [isCurrentSem]) + /** * Determines if the current user has no reviews, so they should receive * the anonymous modal @@ -112,9 +119,16 @@ const ReviewModal = ({ } function onSelectedSemChange(newSem: string) { - setSelectedSem(newSem); - if (SEMESTER_FORMAT.test(newSem)) setValid({ ...valid, semester: true }); - else setValid({ ...valid, semester: false }); + if (newSem.includes("Current")) { + setIsCurrentSem(true); + setSelectedSem(newSem.substring(0,4)); + } else { + setIsCurrentSem(false); + setSelectedSem(newSem) + } + + let validSemFormat = SEMESTER_FORMAT.test(newSem.substring(0,4)); + setValid({ ...valid, semester: validSemFormat }); } function onMajorChange(newSelectedMajors: string[]) { @@ -155,7 +169,8 @@ const ReviewModal = ({ isCovid: false, grade: selectedGrade, major: selectedMajors, - semester: selectedSem + semester: selectedSem, + writtenDuringSemester: isCurrentSem, }; submitReview(newReview); } @@ -244,17 +259,24 @@ const ReviewModal = ({
compareWithCurrentSem(sem) <= 0) + .map((sem) => sem === CURRENT_SEMESTER ? sem + " (Current)" : sem) + } value={selectedSem} onChange={onSelectedSemChange} placeholder="Semester Taken" /> - + {!isCurrentSem && + + }
{ const [loading, setLoading] = useState(true); @@ -91,6 +92,14 @@ const Profile = () => { return 0 } + /** + * Sorts reviews based on descending semester. + */ + const sortBySem = (a: ReviewType, b: ReviewType) => + b.semester && a.semester + ? compareSems(a.semester,b.semester) + : sortByDate(a,b) + /** * Hook that handles * 1. Get + Set reviews @@ -198,6 +207,8 @@ const Profile = () => { setApprovedReviews([...approvedReviews].sort(sortByDate)); } else if (value === 'professor') { setApprovedReviews([...approvedReviews].sort(sortByProf)) + } else if (value === 'semester') { + setApprovedReviews([...approvedReviews].sort(sortBySem)); } } @@ -227,7 +238,9 @@ const Profile = () => {
-

My Reviews ({pendingReviews.length + approvedReviews.length})

+

+ My Reviews ({pendingReviews.length + approvedReviews.length}) +

@@ -249,15 +264,11 @@ const Profile = () => { setHide={setHidePastReviews} pendingReviews={pendingReviews} /> - + )} {reviews.length > 0 && pendingReviews.length === 0 && ( - + )}
diff --git a/client/src/types.ts b/client/src/types.ts index e6d44a8d..5bade22d 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -8,4 +8,5 @@ export type NewReview = { grade: string; major: string[]; semester: string; + writtenDuringSemester: boolean; }; diff --git a/common/CourseCard.js b/common/CourseCard.js index 4179439f..5675518a 100644 --- a/common/CourseCard.js +++ b/common/CourseCard.js @@ -2,6 +2,10 @@ Additonal functions used in the CourseCard component. */ +import { CURRENT_SEMESTER } from './constants'; + +export const SEMESTER_FORMAT = new RegExp('^(SP|SU|WI|FA)\\d{2}$'); + /** * Helper function to convert semester abbreviations to a full word */ @@ -20,6 +24,55 @@ export function semAbbreviationToWord(sem) { } } +/** + * Return full semester label -- eg. "FA20" --> "Fall 2020". + * Assumes 21st century (probably safe for now). + * @param sem abbreviated semester text + */ +export function convertSemAbbreviation(sem) { + return SEMESTER_FORMAT.test(sem) + ? semAbbreviationToWord(sem.substring(0, 2)) + " 20" + sem.substring(2) + : sem +} + +/** + * Comparison function for two semesters. + * Returns -1 if semOne is less than semTwo + * 1 if semOne is greater than semTwo + * 0 otherwise (if they are equal) + */ +export function compareSems(semOne, semTwo) { + if (!SEMESTER_FORMAT.test(semOne) || !SEMESTER_FORMAT.test(semTwo)) { + throw new Error("When comparing semesters, both must be in abbreviated semester format.") + } + + const semOrder = ['WI', 'SP', 'SU', 'FA']; + let semOneTermIndex = semOrder.indexOf(semOne.substring(0,2)) + let semOneYear = parseInt(semOne.substring(2)) + let semTwoTermIndex = semOrder.indexOf(semTwo.substring(0,2)) + let semTwoYear = parseInt(semTwo.substring(2)) + + if (semOneYear < semTwoYear) return -1; + if (semOneYear > semTwoYear) return 1; + if (semOneTermIndex < semTwoTermIndex) return -1; + if (semOneTermIndex > semTwoTermIndex) return 1; + return 0; +} + +/** + * Returns -1 if sem is less than current semester + * 1 if sem is greater than current semester + * 0 otherwise (if they are equal) + */ +export function compareWithCurrentSem(sem) { + if (!SEMESTER_FORMAT.test(sem)) throw new Error("Semester must be in abbreviated format.") + return compareSems(sem, CURRENT_SEMESTER) +} + +export function isCurrentSem(sem) { + return compareWithCurrentSem(sem) === 0; +} + /** * Get a human-readable string representing a list of [up to] the last 2 semesters this class was offered. */ diff --git a/common/constants.ts b/common/constants.ts new file mode 100644 index 00000000..03703245 --- /dev/null +++ b/common/constants.ts @@ -0,0 +1 @@ +export const CURRENT_SEMESTER = "WI25" \ No newline at end of file diff --git a/common/index.d.ts b/common/index.d.ts index 7319f995..fe1b0626 100644 --- a/common/index.d.ts +++ b/common/index.d.ts @@ -56,6 +56,7 @@ export interface Review { grade?: string; major?: string[]; semester?: string; + writtenDuringSemester?: boolean; } export interface Professor { diff --git a/server/db/schema.ts b/server/db/schema.ts index 19ddfac0..cdcb6f07 100644 --- a/server/db/schema.ts +++ b/server/db/schema.ts @@ -123,6 +123,7 @@ const ReviewSchema = new Schema({ grade: { type: String }, major: { type: [String] }, semester: { type: String }, + writtenDuringSemester: { type: Boolean }, // The following was a temporary field used to keep track of reviews for a contest // The full functional code for counting reviews can be found on the following branch: // review-counting-feature diff --git a/server/src/review/review.controller.ts b/server/src/review/review.controller.ts index 9ea508b5..988e2c33 100644 --- a/server/src/review/review.controller.ts +++ b/server/src/review/review.controller.ts @@ -201,6 +201,7 @@ export const insertNewReview = async ({ reported: 0, professors: review.professors, semester: review.semester, + writtenDuringSemester: review.writtenDuringSemester, likes: 0, isCovid: review.isCovid, user: student._id, diff --git a/server/src/review/review.ts b/server/src/review/review.ts index b0e63ca4..5dd8e2c6 100644 --- a/server/src/review/review.ts +++ b/server/src/review/review.ts @@ -17,6 +17,7 @@ type ReviewEntity = { grade: string; major: string[]; semester: string; + writtenDuringSemester: boolean; }; export class Review { @@ -52,6 +53,8 @@ export class Review { private semester: string; + private writtenDuringSemester: boolean; + constructor({ _id, text, @@ -68,7 +71,8 @@ export class Review { grade, major, class: courseId, - semester + semester, + writtenDuringSemester }: ReviewEntity) { this._id = _id; this.text = text; @@ -86,6 +90,7 @@ export class Review { this.major = major; this.class = courseId; this.semester = semester ? semester : ""; + this.writtenDuringSemester = writtenDuringSemester this.validate(); } @@ -115,6 +120,7 @@ export class Review { grade: joi.optional(), major: joi.optional(), semester: joi.optional(), + writtenDuringSemester: joi.optional(), }); const { error } = searchSchema.validate(this); diff --git a/server/src/review/review.type.ts b/server/src/review/review.type.ts index f289bb13..b3abb967 100644 --- a/server/src/review/review.type.ts +++ b/server/src/review/review.type.ts @@ -36,6 +36,7 @@ interface ReviewRequestType { grade: string; major: string[]; semester: string; + writtenDuringSemester: boolean; } export interface ReviewLikesRequestType {